diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 8623b8c3ca107..3c2a120d167d9 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -96,13 +96,37 @@ All columns that belong to the same layer pane group are sorted in the table. * *Text alignment* — Aligns the values in the cell to the *Left*, *Center*, or *Right*. +* *Color by value* — Applies color to the cell or text values. To change the color, click the *Edit colors* icon. + * *Hide column* — Hides the column for the field. * *Directly filter on click* — Turns column values into clickable links that allow you to filter or drill down into the data. * *Summary row* — Adds a row that displays the summary value. When specified, allows you to enter a *Summary label*. -* *Color by value* — Applies color to the cell or text values. To change the color, click *Edit*. +[float] +[[assign-colors-to-terms]] +===== Assign colors to terms + +preview::[] + +For term-based metrics, assign a color to each term with color mapping. + +. Create a custom table. + +. In the layer pane, select a *Rows* or *Metrics* field. + +. In the *Color by value* option, select *Cell* or *Text*. + +. Click the *Edit colors* icon. + +. Toggle the button to use the Color Mapping feature. + +. Select a color palette and mode. + +. Click *Add assignment* to assign a color to a specific term, or click *Add all unassigned terms* to assign colors to all terms. Assigning colors to dates is unsupported. + +. Configure color assignments. You can also select whether unassigned terms should be mapped to the selected color palette or a single color. [float] [[drag-and-drop-keyboard-navigation]] diff --git a/package.json b/package.json index afda7cd4c9125..0ad1d96d7b5db 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@elastic/datemath": "5.0.3", "@elastic/ebt": "^1.1.1", "@elastic/ecs": "^8.11.1", - "@elastic/elasticsearch": "^8.15.0", + "@elastic/elasticsearch": "^8.15.1", "@elastic/ems-client": "8.5.3", "@elastic/eui": "97.3.0", "@elastic/filesaver": "1.1.2", @@ -1498,7 +1498,7 @@ "@octokit/rest": "^17.11.2", "@parcel/watcher": "^2.1.0", "@playwright/test": "=1.46.0", - "@redocly/cli": "^1.25.10", + "@redocly/cli": "^1.25.11", "@statoscope/webpack-plugin": "^5.28.2", "@storybook/addon-a11y": "^6.5.16", "@storybook/addon-actions": "^6.5.16", diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts index 1aeabb7e86dea..c84b30cf15774 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/catch_retryable_es_client_errors.test.ts @@ -72,24 +72,25 @@ describe('catchRetryableEsClientErrors', () => { type: 'retryable_es_client_error', }); }); - it('ResponseError with retryable status code', async () => { - const statusCodes = [503, 401, 403, 408, 410, 429]; - return Promise.all( - statusCodes.map(async (status) => { - const error = new esErrors.ResponseError( - elasticsearchClientMock.createApiResponse({ - statusCode: status, - body: { error: { type: 'reason' } }, - }) - ); - expect( - ((await Promise.reject(error).catch(catchRetryableEsClientErrors)) as any).left - ).toMatchObject({ - message: 'reason', - type: 'retryable_es_client_error', - }); - }) - ); - }); + it.each([503, 401, 403, 408, 410, 429])( + 'ResponseError with retryable status code (%d)', + async (status) => { + const error = new esErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: status, + body: { error: { type: 'reason' } }, + }) + ); + expect( + ((await Promise.reject(error).catch(catchRetryableEsClientErrors)) as any).left + ).toMatchObject({ + message: + status === 410 + ? 'This API is unavailable in the version of Elasticsearch you are using.' + : 'reason', + type: 'retryable_es_client_error', + }); + } + ); }); }); diff --git a/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts b/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts index ed6d1b813184b..7fd639331af80 100644 --- a/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts +++ b/packages/kbn-apm-synthtrace/src/lib/shared/base_client.ts @@ -55,7 +55,6 @@ export class SynthtraceEsClient { await this.client.indices.resolveIndex({ name: this.indices.join(','), expand_wildcards: ['open', 'hidden'], - // @ts-expect-error ignore_unavailable is not in the type definition, but it is accepted by es ignore_unavailable: true, }) ).indices.map((index: { name: string }) => index.name) diff --git a/src/plugins/controls/common/constants.ts b/src/plugins/controls/common/constants.ts index d1434d4df2ae0..afd6fe66f0df1 100644 --- a/src/plugins/controls/common/constants.ts +++ b/src/plugins/controls/common/constants.ts @@ -16,7 +16,7 @@ export const CONTROL_CHAINING_OPTIONS = { NONE: 'NONE', HIERARCHICAL: 'HIERARCHI export const DEFAULT_CONTROL_WIDTH: ControlWidth = CONTROL_WIDTH_OPTIONS.MEDIUM; export const DEFAULT_CONTROL_LABEL_POSITION: ControlLabelPosition = CONTROL_LABEL_POSITION_OPTIONS.ONE_LINE; -export const DEFAULT_CONTROL_GROW: boolean = true; +export const DEFAULT_CONTROL_GROW: boolean = false; export const DEFAULT_CONTROL_CHAINING: ControlGroupChainingSystem = CONTROL_CHAINING_OPTIONS.HIERARCHICAL; export const DEFAULT_IGNORE_PARENT_SETTINGS = { diff --git a/src/plugins/controls/public/control_group/components/control_group_editor.tsx b/src/plugins/controls/public/control_group/components/control_group_editor.tsx index 8f1ccb4d699b0..cb21c23bc9ce4 100644 --- a/src/plugins/controls/public/control_group/components/control_group_editor.tsx +++ b/src/plugins/controls/public/control_group/components/control_group_editor.tsx @@ -72,7 +72,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager return ( <> - +

{ControlGroupStrings.management.getFlyoutTitle()}

@@ -80,7 +80,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager { onCancel(); }} @@ -204,7 +204,7 @@ export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager { diff --git a/src/plugins/controls/public/control_group/control_group_strings.tsx b/src/plugins/controls/public/control_group/control_group_strings.tsx index b8f6a11abf839..f5c92d987b271 100644 --- a/src/plugins/controls/public/control_group/control_group_strings.tsx +++ b/src/plugins/controls/public/control_group/control_group_strings.tsx @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; export const ControlGroupStrings = { getSaveChangesTitle: () => i18n.translate('controls.controlGroup.manageControl.saveChangesTitle', { - defaultMessage: 'Save and close', + defaultMessage: 'Save', }), getCancelTitle: () => i18n.translate('controls.controlGroup.manageControl.cancelTitle', { diff --git a/src/plugins/controls/public/control_group/init_controls_manager.test.ts b/src/plugins/controls/public/control_group/init_controls_manager.test.ts index 29998325664bb..d88dc5452a0e5 100644 --- a/src/plugins/controls/public/control_group/init_controls_manager.test.ts +++ b/src/plugins/controls/public/control_group/init_controls_manager.test.ts @@ -263,7 +263,7 @@ describe('getNewControlState', () => { test('should contain defaults when there are no existing controls', () => { const controlsManager = initControlsManager({}, new BehaviorSubject({})); expect(controlsManager.getNewControlState()).toEqual({ - grow: true, + grow: false, width: 'medium', dataViewId: undefined, }); @@ -284,7 +284,7 @@ describe('getNewControlState', () => { new BehaviorSubject(intialControlsState) ); expect(controlsManager.getNewControlState()).toEqual({ - grow: true, + grow: false, width: 'medium', dataViewId: 'myOtherDataViewId', }); diff --git a/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx b/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx index 54e35ab271b34..459913d98de0b 100644 --- a/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx +++ b/src/plugins/controls/public/control_group/open_edit_control_group_flyout.tsx @@ -101,6 +101,9 @@ export const openEditControlGroupFlyout = ( 'aria-label': i18n.translate('controls.controlGroup.manageControl', { defaultMessage: 'Edit control settings', }), + size: 'm', + maxWidth: 500, + paddingSize: 'm', outsideClickCloses: false, onClose: () => closeOverlay(overlay), } diff --git a/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx b/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx index 6b06bd8a52439..23d4c68f6c5dc 100644 --- a/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx +++ b/src/plugins/controls/public/controls/data_controls/data_control_constants.tsx @@ -21,14 +21,6 @@ export const DataControlEditorStrings = { defaultMessage: 'Edit control', }), dataSource: { - getFormGroupTitle: () => - i18n.translate('controls.controlGroup.manageControl.dataSource.formGroupTitle', { - defaultMessage: 'Data source', - }), - getFormGroupDescription: () => - i18n.translate('controls.controlGroup.manageControl.dataSource.formGroupDescription', { - defaultMessage: 'Select the data view and field that you want to create a control for.', - }), getSelectDataViewMessage: () => i18n.translate('controls.controlGroup.manageControl.dataSource.selectDataViewMessage', { defaultMessage: 'Please select a data view', @@ -95,14 +87,6 @@ export const DataControlEditorStrings = { }, }, displaySettings: { - getFormGroupTitle: () => - i18n.translate('controls.controlGroup.manageControl.displaySettings.formGroupTitle', { - defaultMessage: 'Display settings', - }), - getFormGroupDescription: () => - i18n.translate('controls.controlGroup.manageControl.displaySettings.formGroupDescription', { - defaultMessage: 'Change how the control appears on your dashboard.', - }), getTitleInputTitle: () => i18n.translate('controls.controlGroup.manageControl.displaySettings.titleInputTitle', { defaultMessage: 'Label', @@ -133,7 +117,7 @@ export const DataControlEditorStrings = { }, getSaveChangesTitle: () => i18n.translate('controls.controlGroup.manageControl.saveChangesTitle', { - defaultMessage: 'Save and close', + defaultMessage: 'Save', }), getCancelTitle: () => i18n.translate('controls.controlGroup.manageControl.cancelTitle', { diff --git a/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx b/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx index 23fd95978ff82..a84425f350dc1 100644 --- a/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx +++ b/src/plugins/controls/public/controls/data_controls/data_control_editor.tsx @@ -15,7 +15,6 @@ import { EuiButtonEmpty, EuiButtonGroup, EuiCallOut, - EuiDescribedFormGroup, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -250,20 +249,8 @@ export const DataControlEditor = - {DataControlEditorStrings.manageControl.controlTypeSettings.getFormGroupTitle( - controlFactory.getDisplayName() - )} - - } - description={DataControlEditorStrings.manageControl.controlTypeSettings.getFormGroupDescription( - controlFactory.getDisplayName() - )} - data-test-subj="control-editor-custom-settings" - > +
+ - +
); }, [fieldRegistry, controlFactory, initialState, editorState, controlGroupApi]); return ( <> - +

{!controlId // if no ID, then we are creating a new control ? DataControlEditorStrings.manageControl.getFlyoutCreateTitle() @@ -288,156 +275,144 @@ export const DataControlEditor = - {DataControlEditorStrings.manageControl.dataSource.getFormGroupTitle()}

} - description={DataControlEditorStrings.manageControl.dataSource.getFormGroupDescription()} - > - {!editorConfig?.hideDataViewSelector && ( - - {dataViewListError ? ( - -

{dataViewListError.message}

-
- ) : ( - { - setEditorState({ ...editorState, dataViewId: newDataViewId }); - setSelectedControlType(undefined); - }} - trigger={{ - label: - selectedDataView?.getName() ?? - DataControlEditorStrings.manageControl.dataSource.getSelectDataViewMessage(), - }} - selectableProps={{ isLoading: dataViewListLoading }} - /> - )} -
- )} - - - {fieldListError ? ( + {!editorConfig?.hideDataViewSelector && ( + + {dataViewListError ? ( -

{fieldListError.message}

+

{dataViewListError.message}

) : ( - { - const customPredicate = editorConfig?.fieldFilterPredicate?.(field) ?? true; - return Boolean(fieldRegistry?.[field.name]) && customPredicate; + { + setEditorState({ ...editorState, dataViewId: newDataViewId }); + setSelectedControlType(undefined); }} - selectedFieldName={editorState.fieldName} - dataView={selectedDataView} - onSelectField={(field) => { - setEditorState({ ...editorState, fieldName: field.name }); - - /** - * make sure that the new field is compatible with the selected control type and, if it's not, - * reset the selected control type to the **first** compatible control type - */ - const newCompatibleControlTypes = - fieldRegistry?.[field.name]?.compatibleControlTypes ?? []; - if ( - !selectedControlType || - !newCompatibleControlTypes.includes(selectedControlType!) - ) { - setSelectedControlType(newCompatibleControlTypes[0]); - } - - /** - * set the control title (i.e. the one set by the user) + default title (i.e. the field display name) - */ - const newDefaultTitle = field.displayName ?? field.name; - setDefaultPanelTitle(newDefaultTitle); - const currentTitle = editorState.title; - if (!currentTitle || currentTitle === newDefaultTitle) { - setPanelTitle(newDefaultTitle); - } - - setControlOptionsValid(true); // reset options state + trigger={{ + label: + selectedDataView?.getName() ?? + DataControlEditorStrings.manageControl.dataSource.getSelectDataViewMessage(), }} - selectableProps={{ isLoading: dataViewListLoading || dataViewLoading }} + selectableProps={{ isLoading: dataViewListLoading }} /> )}
+ )} + + + {fieldListError ? ( + +

{fieldListError.message}

+
+ ) : ( + { + const customPredicate = editorConfig?.fieldFilterPredicate?.(field) ?? true; + return Boolean(fieldRegistry?.[field.name]) && customPredicate; + }} + selectedFieldName={editorState.fieldName} + dataView={selectedDataView} + onSelectField={(field) => { + setEditorState({ ...editorState, fieldName: field.name }); + + /** + * make sure that the new field is compatible with the selected control type and, if it's not, + * reset the selected control type to the **first** compatible control type + */ + const newCompatibleControlTypes = + fieldRegistry?.[field.name]?.compatibleControlTypes ?? []; + if ( + !selectedControlType || + !newCompatibleControlTypes.includes(selectedControlType!) + ) { + setSelectedControlType(newCompatibleControlTypes[0]); + } + + /** + * set the control title (i.e. the one set by the user) + default title (i.e. the field display name) + */ + const newDefaultTitle = field.displayName ?? field.name; + setDefaultPanelTitle(newDefaultTitle); + const currentTitle = editorState.title; + if (!currentTitle || currentTitle === newDefaultTitle) { + setPanelTitle(newDefaultTitle); + } + + setControlOptionsValid(true); // reset options state + }} + selectableProps={{ isLoading: dataViewListLoading || dataViewLoading }} + /> + )} +
+ + {/* wrapping in `div` so that focus gets passed properly to the form row */} +
+ +
+
+ + { + setPanelTitle(e.target.value ?? ''); + setEditorState({ + ...editorState, + title: e.target.value === '' ? undefined : e.target.value, + }); + }} + /> + + {!editorConfig?.hideWidthSettings && ( - {/* wrapping in `div` so that focus gets passed properly to the form row */}
- + setEditorState({ ...editorState, width: newWidth as ControlWidth }) + } + /> + + setEditorState({ ...editorState, grow: !editorState.grow })} + data-test-subj="control-editor-grow-switch" />
- - {DataControlEditorStrings.manageControl.displaySettings.getFormGroupTitle()} - } - description={DataControlEditorStrings.manageControl.displaySettings.getFormGroupDescription()} - > - - { - setPanelTitle(e.target.value ?? ''); - setEditorState({ - ...editorState, - title: e.target.value === '' ? undefined : e.target.value, - }); - }} - /> - - {!editorConfig?.hideWidthSettings && ( - -
- - setEditorState({ ...editorState, width: newWidth as ControlWidth }) - } - /> - - setEditorState({ ...editorState, grow: !editorState.grow })} - data-test-subj="control-editor-grow-switch" - /> -
-
- )} -
+ )} {!editorConfig?.hideAdditionalSettings && CustomSettingsComponent} {controlId && ( <> @@ -464,7 +439,6 @@ export const DataControlEditor = { onCancel(editorState); }} @@ -476,7 +450,7 @@ export const DataControlEditor = closeOverlay(overlay), } ); diff --git a/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx b/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx index e9dad12be5623..f07a7cc6c58bf 100644 --- a/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx +++ b/src/plugins/controls/public/controls/data_controls/options_list_control/components/options_list_editor_options.tsx @@ -131,6 +131,7 @@ export const OptionsListEditorOptions = ({ data-test-subj="optionsListControl__selectionOptionsRadioGroup" > { @@ -146,6 +147,7 @@ export const OptionsListEditorOptions = ({ data-test-subj="optionsListControl__searchOptionsRadioGroup" > { @@ -158,6 +160,7 @@ export const OptionsListEditorOptions = ({ )} { const newStep = event.target.valueAsNumber; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx index dbb86046def06..e6adece8ab36d 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/add_new_panel/dashboard_panel_selection_flyout.tsx @@ -125,7 +125,7 @@ export const DashboardPanelSelectionListFlyout: React.FC< return ( <> - +

{ @@ -281,7 +282,7 @@ export const DashboardPanelSelectionListFlyout: React.FC< diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx index 2cad63c442026..cf7f9c65c6618 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.tsx @@ -43,7 +43,7 @@ export const EditorMenu = ({ createNewVisType, isDisabled }: EditorMenuProps) => function openDashboardPanelSelectionFlyout() { const flyoutPanelPaddingSize: ComponentProps< typeof DashboardPanelSelectionListFlyout - >['paddingSize'] = 'l'; + >['paddingSize'] = 'm'; const mount = toMountPoint( React.createElement(function () { diff --git a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx index b83ebbcb49d66..b334dbcb5857a 100644 --- a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx @@ -189,7 +189,7 @@ export const AddPanelFlyout = ({ return ( <> - +

{i18n.translate('embeddableApi.addPanel.Title', { defaultMessage: 'Add from library' })}

diff --git a/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx index 160289d0d1c2a..9ba3c00a73745 100644 --- a/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/add_panel_flyout/open_add_panel_flyout.tsx @@ -52,6 +52,9 @@ export const openAddPanelFlyout = ({ if (onClose) onClose(); overlayRef.close(); }, + size: 'm', + maxWidth: 500, + paddingSize: 'm', 'data-test-subj': 'dashboardAddPanel', 'aria-labelledby': modalTitleId, } diff --git a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx index 265f162d04f6c..f052f0526a945 100644 --- a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx +++ b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.test.tsx @@ -43,11 +43,11 @@ const ImageEditor = (props: Partial) => { ); }; -test('should call onCancel when "Close" clicked', async () => { +test('should call onCancel when "Cancel" clicked', async () => { const onCancel = jest.fn(); const { getByText } = render(); - expect(getByText('Close')).toBeVisible(); - await userEvent.click(getByText('Close')); + expect(getByText('Cancel')).toBeVisible(); + await userEvent.click(getByText('Cancel')); expect(onCancel).toBeCalled(); }); diff --git a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx index 2c57f25db6c8b..1a5ee3bc64e1d 100644 --- a/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx +++ b/src/plugins/image_embeddable/public/components/image_editor/image_editor_flyout.tsx @@ -121,7 +121,7 @@ export function ImageEditorFlyout(props: ImageEditorFlyoutProps) { return ( <> - +

{isEditing ? ( - - + + + setSrcType('file')} isSelected={srcType === 'file'}> - - + {srcType === 'file' && ( <> {isDraftImageConfigValid ? ( @@ -238,7 +238,7 @@ export function ImageEditorFlyout(props: ImageEditorFlyoutProps) { />

} - titleSize={'s'} + titleSize={'xs'} /> ) : ( )} - - + )} - - - - - - + diff --git a/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx b/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx index ae8ced88d14ef..f730147cb0d2c 100644 --- a/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx +++ b/src/plugins/image_embeddable/public/components/image_editor/open_image_editor.tsx @@ -79,6 +79,9 @@ export const openImageEditor = async ({ onClose: () => { onCancel(); }, + size: 'm', + maxWidth: 500, + paddingSize: 'm', ownFocus: true, 'data-test-subj': 'createImageEmbeddableFlyout', } diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx index b062b9befa284..ab5b923327049 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_destination_picker.tsx @@ -119,6 +119,7 @@ export const DashboardLinkDestinationPicker = ({ return ( onClose()} > - +

{link ? LinksStrings.editor.getEditLinkTitle() @@ -113,6 +113,7 @@ export const LinkEditor = ({ { @@ -131,6 +132,7 @@ export const LinkEditor = ({ /> onClose()} - iconType="cross" data-test-subj="links--linkEditor--closeBtn" > {LinksStrings.editor.getCancelButtonLabel()} @@ -160,6 +162,7 @@ export const LinkEditor = ({ { // this check should always be true, since the button is disabled otherwise - this is just for type safety diff --git a/src/plugins/links/public/components/editor/links_editor.scss b/src/plugins/links/public/components/editor/links_editor.scss index 02961c7d5f5cb..c33b95350df98 100644 --- a/src/plugins/links/public/components/editor/links_editor.scss +++ b/src/plugins/links/public/components/editor/links_editor.scss @@ -3,7 +3,7 @@ .linksPanelEditor { .linkEditor { @include euiFlyout; - max-inline-size: $euiSizeXXL * 18; // 40px * 18 = 720px + max-inline-size: $euiSizeXS * 125; // 4px * 125 = 500px &.in { animation: euiFlyoutOpenAnimation $euiAnimSpeedNormal $euiAnimSlightResistance; @@ -59,6 +59,9 @@ } .links_hoverActions { + background-color: $euiColorEmptyShade; + position: absolute; + right: $euiSizeL; opacity: 0; visibility: hidden; transition: visibility $euiAnimSpeedNormal, opacity $euiAnimSpeedNormal; diff --git a/src/plugins/links/public/components/editor/links_editor.tsx b/src/plugins/links/public/components/editor/links_editor.tsx index 93ca47e364c57..8fa33fd4ebcaa 100644 --- a/src/plugins/links/public/components/editor/links_editor.tsx +++ b/src/plugins/links/public/components/editor/links_editor.tsx @@ -167,7 +167,7 @@ const LinksEditor = ({ - +

{isEditingExisting ? LinksStrings.editor.panelEditor.getEditFlyoutTitle() @@ -251,7 +251,6 @@ const LinksEditor = ({ @@ -268,6 +267,7 @@ const LinksEditor = ({ data-test-subj="links--panelEditor--saveByReferenceTooltip" > - - - - - - - - - - - - - - + + + + + + + + + + + + ); diff --git a/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx b/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx index dfdc6e0589e6b..5b8522b39960e 100644 --- a/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx +++ b/src/plugins/links/public/components/external_link/external_link_destination_picker.tsx @@ -54,6 +54,7 @@ export const ExternalLinkDestinationPicker = ({ return (
i18n.translate('links.editor.cancelButtonLabel', { - defaultMessage: 'Close', + defaultMessage: 'Cancel', }), panelEditor: { getLinksTitle: () => diff --git a/src/plugins/links/public/editor/open_editor_flyout.tsx b/src/plugins/links/public/editor/open_editor_flyout.tsx index 041672e89dbbe..87b1ab4e21ff8 100644 --- a/src/plugins/links/public/editor/open_editor_flyout.tsx +++ b/src/plugins/links/public/editor/open_editor_flyout.tsx @@ -137,7 +137,8 @@ export async function openEditorFlyout({ ), { id: flyoutId, - maxWidth: 720, + maxWidth: 500, + paddingSize: 'm', ownFocus: true, onClose: onCancel, outsideClickCloses: false, diff --git a/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx index 921560f7a2224..63ff89da2ec17 100644 --- a/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx +++ b/src/plugins/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx @@ -8,7 +8,7 @@ */ import React from 'react'; -import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; import { DashboardDrilldownOptions } from './types'; import { dashboardDrilldownConfigStrings } from '../../i18n/dashboard_drilldown_config'; @@ -24,32 +24,35 @@ export const DashboardDrilldownOptionsComponent = ({ }: DashboardDrilldownOptionsProps) => { return ( <> - - onOptionChange({ useCurrentFilters: !options.useCurrentFilters })} - data-test-subj="dashboardDrillDownOptions--useCurrentFilters--checkbox" - /> - - - onOptionChange({ useCurrentDateRange: !options.useCurrentDateRange })} - data-test-subj="dashboardDrillDownOptions--useCurrentDateRange--checkbox" - /> - - - onOptionChange({ openInNewTab: !options.openInNewTab })} - data-test-subj="dashboardDrillDownOptions--openInNewTab--checkbox" - /> + +
+ onOptionChange({ useCurrentFilters: !options.useCurrentFilters })} + data-test-subj="dashboardDrillDownOptions--useCurrentFilters--checkbox" + /> + + onOptionChange({ useCurrentDateRange: !options.useCurrentDateRange })} + data-test-subj="dashboardDrillDownOptions--useCurrentDateRange--checkbox" + /> + + onOptionChange({ openInNewTab: !options.openInNewTab })} + data-test-subj="dashboardDrillDownOptions--openInNewTab--checkbox" + /> +
); diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx index e985e9bec357a..1c8466097bd98 100644 --- a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -52,6 +52,7 @@ export function DataViewPicker({ data-test-subj="open-data-view-picker" onClick={() => setPopoverIsOpen(!isPopoverOpen)} label={label} + size="s" fullWidth {...colorProp} {...rest} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx index daac202f21b66..0b81cfd66156d 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -140,6 +140,7 @@ export const FieldPicker = ({ placeholder: i18n.translate('presentationUtil.fieldSearch.searchPlaceHolder', { defaultMessage: 'Search field names', }), + compressed: true, disabled: Boolean(selectableProps?.isLoading), inputRef: setSearchRef, }} diff --git a/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx b/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx index d2e929b8a9a84..4212668599a0d 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_type_filter.tsx @@ -63,7 +63,7 @@ export function FieldTypeFilter({ ); return ( - + { return ( <> - - onOptionChange({ openInNewTab: !options.openInNewTab })} - data-test-subj="urlDrilldownOpenInNewTab" - /> - - - - {txtUrlTemplateEncodeUrl} - - {txtUrlTemplateEncodeDescription} - - } - checked={options.encodeUrl} - onChange={() => onOptionChange({ encodeUrl: !options.encodeUrl })} - data-test-subj="urlDrilldownEncodeUrl" - /> + +
+ onOptionChange({ openInNewTab: !options.openInNewTab })} + data-test-subj="urlDrilldownOpenInNewTab" + /> + + + {txtUrlTemplateEncodeUrl} + + {txtUrlTemplateEncodeDescription} + + } + checked={options.encodeUrl} + onChange={() => onOptionChange({ encodeUrl: !options.encodeUrl })} + data-test-subj="urlDrilldownEncodeUrl" + /> +
); diff --git a/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts b/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts index 2b0fa0dab4424..a80ca89ee4865 100644 --- a/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts +++ b/test/api_integration/apis/data_views/fields_for_wildcard_route/response.ts @@ -80,7 +80,8 @@ export default function ({ getService }: FtrProviderContext) { }, ]; - describe('fields_for_wildcard_route response', () => { + // Failing: See https://github.com/elastic/kibana/issues/199413 + describe.skip('fields_for_wildcard_route response', () => { before(() => esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') ); diff --git a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts index 037a9e809d1e1..09dc85b816191 100644 --- a/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts +++ b/x-pack/packages/ai-infra/product-doc-artifact-builder/src/tasks/install_elser.ts @@ -60,7 +60,6 @@ const waitUntilDeployed = async ({ model_id: modelId, }); const deploymentStats = statsRes.trained_model_stats[0]?.deployment_stats; - // @ts-expect-error deploymentStats.nodes not defined as array even if it is. if (!deploymentStats || deploymentStats.nodes.length === 0) { await sleep(delay); continue; diff --git a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts index 3d37331b4cc5d..076c685aca5b9 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/schema/graph/v1.ts @@ -6,14 +6,26 @@ */ import { schema } from '@kbn/config-schema'; +import { ApiMessageCode } from '../../types/graph/v1'; export const graphRequestSchema = schema.object({ + nodesLimit: schema.maybe(schema.number()), + showUnknownTarget: schema.maybe(schema.boolean()), query: schema.object({ - actorIds: schema.arrayOf(schema.string()), eventIds: schema.arrayOf(schema.string()), // TODO: use zod for range validation instead of config schema start: schema.oneOf([schema.number(), schema.string()]), end: schema.oneOf([schema.number(), schema.string()]), + esQuery: schema.maybe( + schema.object({ + bool: schema.object({ + filter: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + should: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + must_not: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + }), + }) + ), }), }); @@ -23,6 +35,9 @@ export const graphResponseSchema = () => schema.oneOf([entityNodeDataSchema, groupNodeDataSchema, labelNodeDataSchema]) ), edges: schema.arrayOf(edgeDataSchema), + messages: schema.maybe( + schema.arrayOf(schema.oneOf([schema.literal(ApiMessageCode.ReachedNodesLimit)])) + ), }); export const colorSchema = schema.oneOf([ diff --git a/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json b/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json index c7cf1e9208bfc..ebec9929559f0 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json +++ b/x-pack/packages/kbn-cloud-security-posture/common/tsconfig.json @@ -20,5 +20,6 @@ "@kbn/i18n", "@kbn/analytics", "@kbn/usage-collection-plugin", + "@kbn/es-query", ] } diff --git a/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts b/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts index 48d1d1c49fd03..f97d11b34732c 100644 --- a/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts +++ b/x-pack/packages/kbn-cloud-security-posture/common/types/graph/v1.ts @@ -6,6 +6,7 @@ */ import type { TypeOf } from '@kbn/config-schema'; +import type { BoolQuery } from '@kbn/es-query'; import { colorSchema, edgeDataSchema, @@ -17,13 +18,21 @@ import { nodeShapeSchema, } from '../../schema/graph/v1'; -export type GraphRequest = TypeOf; -export type GraphResponse = TypeOf; +export type GraphRequest = Omit, 'query.esQuery'> & { + query: { esQuery?: { bool: Partial } }; +}; +export type GraphResponse = Omit, 'messages'> & { + messages?: ApiMessageCode[]; +}; export type Color = typeof colorSchema.type; export type NodeShape = TypeOf; +export enum ApiMessageCode { + ReachedNodesLimit = 'REACHED_NODES_LIMIT', +} + export type EntityNodeDataModel = TypeOf; export type GroupNodeDataModel = TypeOf; diff --git a/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts b/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts index 5054833ac7dd0..39d26509422a2 100644 --- a/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts +++ b/x-pack/packages/ml/random_sampler_utils/src/random_sampler_wrapper.ts @@ -69,7 +69,6 @@ export const createRandomSamplerWrapper = (options: RandomSamplerOptions) => { return { [aggName]: { - // @ts-expect-error `random_sampler` is not yet part of `AggregationsAggregationContainer` random_sampler: { probability, ...(options.seed ? { seed: options.seed } : {}), diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts index 9e9744b33d940..9fb817b275a0d 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/route.ts @@ -10,6 +10,7 @@ import { graphResponseSchema, } from '@kbn/cloud-security-posture-common/schema/graph/latest'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/v1'; import { GRAPH_ROUTE_PATH } from '../../../common/constants'; import { CspRouter } from '../../types'; import { getGraph as getGraphV1 } from './v1'; @@ -39,26 +40,29 @@ export const defineGraphRoute = (router: CspRouter) => }, }, async (context, request, response) => { - const { actorIds, eventIds, start, end } = request.body.query; + const { nodesLimit, showUnknownTarget = false } = request.body; + const { eventIds, start, end, esQuery } = request.body.query as GraphRequest['query']; const cspContext = await context.csp; const spaceId = (await cspContext.spaces?.spacesService?.getActiveSpace(request))?.id; try { - const { nodes, edges } = await getGraphV1( - { + const resp = await getGraphV1({ + services: { logger: cspContext.logger, esClient: cspContext.esClient, }, - { - actorIds, + query: { eventIds, spaceId, start, end, - } - ); + esQuery, + }, + showUnknownTarget, + nodesLimit, + }); - return response.ok({ body: { nodes, edges } }); + return response.ok({ body: resp }); } catch (err) { const error = transformError(err); cspContext.logger.error(`Failed to fetch graph ${err}`); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/types.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/types.ts deleted file mode 100644 index ba32664da6233..0000000000000 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/types.ts +++ /dev/null @@ -1,23 +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 type { - EdgeDataModel, - NodeDataModel, -} from '@kbn/cloud-security-posture-common/types/graph/latest'; -import type { Logger, IScopedClusterClient } from '@kbn/core/server'; -import type { Writable } from '@kbn/utility-types'; - -export interface GraphContextServices { - logger: Logger; - esClient: IScopedClusterClient; -} - -export interface GraphContext { - nodes: Array>; - edges: Array>; -} diff --git a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts index 5102d153c1905..b14a2ba3e06a9 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/graph/v1.ts @@ -8,22 +8,27 @@ import { castArray } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import type { Logger, IScopedClusterClient } from '@kbn/core/server'; +import { ApiMessageCode } from '@kbn/cloud-security-posture-common/types/graph/latest'; import type { + Color, EdgeDataModel, - NodeDataModel, EntityNodeDataModel, - LabelNodeDataModel, + GraphRequest, + GraphResponse, GroupNodeDataModel, -} from '@kbn/cloud-security-posture-common/types/graph/latest'; + LabelNodeDataModel, + NodeDataModel, +} from '@kbn/cloud-security-posture-common/types/graph/v1'; import type { EsqlToRecords } from '@elastic/elasticsearch/lib/helpers'; import type { Writable } from '@kbn/utility-types'; -import type { GraphContextServices, GraphContext } from './types'; + +type EsQuery = GraphRequest['query']['esQuery']; interface GraphEdge { badge: number; - ips: string[]; - hosts: string[]; - users: string[]; + ips?: string[] | string; + hosts?: string[] | string; + users?: string[] | string; actorIds: string[] | string; action: string; targetIds: string[] | string; @@ -36,50 +41,75 @@ interface LabelEdges { target: string; } -export const getGraph = async ( - services: GraphContextServices, +interface GraphContextServices { + logger: Logger; + esClient: IScopedClusterClient; +} + +interface GetGraphParams { + services: GraphContextServices; query: { - actorIds: string[]; eventIds: string[]; spaceId?: string; start: string | number; end: string | number; - } -): Promise<{ - nodes: NodeDataModel[]; - edges: EdgeDataModel[]; -}> => { - const { esClient, logger } = services; - const { actorIds, eventIds, spaceId = 'default', start, end } = query; - - logger.trace( - `Fetching graph for [eventIds: ${eventIds.join(', ')}] [actorIds: ${actorIds.join( - ', ' - )}] in [spaceId: ${spaceId}]` - ); + esQuery?: EsQuery; + }; + showUnknownTarget: boolean; + nodesLimit?: number; +} - const results = await fetchGraph({ esClient, logger, start, end, eventIds, actorIds }); +export const getGraph = async ({ + services: { esClient, logger }, + query: { eventIds, spaceId = 'default', start, end, esQuery }, + showUnknownTarget, + nodesLimit, +}: GetGraphParams): Promise> => { + logger.trace(`Fetching graph for [eventIds: ${eventIds.join(', ')}] in [spaceId: ${spaceId}]`); + + const results = await fetchGraph({ + esClient, + showUnknownTarget, + logger, + start, + end, + eventIds, + esQuery, + }); // Convert results into set of nodes and edges - const graphContext = parseRecords(logger, results.records); - - return { nodes: graphContext.nodes, edges: graphContext.edges }; + return parseRecords(logger, results.records, nodesLimit); }; interface ParseContext { - nodesMap: Record; - edgesMap: Record; - edgeLabelsNodes: Record; - labelEdges: Record; + readonly nodesLimit?: number; + readonly nodesMap: Record; + readonly edgesMap: Record; + readonly edgeLabelsNodes: Record; + readonly labelEdges: Record; + readonly messages: ApiMessageCode[]; + readonly logger: Logger; } -const parseRecords = (logger: Logger, records: GraphEdge[]): GraphContext => { - const ctx: ParseContext = { nodesMap: {}, edgeLabelsNodes: {}, edgesMap: {}, labelEdges: {} }; +const parseRecords = ( + logger: Logger, + records: GraphEdge[], + nodesLimit?: number +): Pick => { + const ctx: ParseContext = { + nodesLimit, + logger, + nodesMap: {}, + edgeLabelsNodes: {}, + edgesMap: {}, + labelEdges: {}, + messages: [], + }; - logger.trace(`Parsing records [length: ${records.length}]`); + logger.trace(`Parsing records [length: ${records.length}] [nodesLimit: ${nodesLimit ?? 'none'}]`); - createNodes(logger, records, ctx); - createEdgesAndGroups(logger, ctx); + createNodes(records, ctx); + createEdgesAndGroups(ctx); logger.trace( `Parsed [nodes: ${Object.keys(ctx.nodesMap).length}, edges: ${ @@ -90,7 +120,11 @@ const parseRecords = (logger: Logger, records: GraphEdge[]): GraphContext => { // Sort groups to be first (fixes minor layout issue) const nodes = sortNodes(ctx.nodesMap); - return { nodes, edges: Object.values(ctx.edgesMap) }; + return { + nodes, + edges: Object.values(ctx.edgesMap), + messages: ctx.messages.length > 0 ? ctx.messages : undefined, + }; }; const fetchGraph = async ({ @@ -98,15 +132,17 @@ const fetchGraph = async ({ logger, start, end, - actorIds, eventIds, + showUnknownTarget, + esQuery, }: { esClient: IScopedClusterClient; logger: Logger; start: string | number; end: string | number; - actorIds: string[]; eventIds: string[]; + showUnknownTarget: boolean; + esQuery?: EsQuery; }): Promise> => { const query = `from logs-* | WHERE event.action IS NOT NULL AND actor.entity.id IS NOT NULL @@ -124,59 +160,84 @@ const fetchGraph = async ({ targetIds = target.entity.id, eventOutcome = event.outcome, isAlert -| LIMIT 1000`; +| LIMIT 1000 +| SORT isAlert DESC`; logger.trace(`Executing query [${query}]`); return await esClient.asCurrentUser.helpers .esql({ columnar: false, - filter: { - bool: { - must: [ + filter: buildDslFilter(eventIds, showUnknownTarget, start, end, esQuery), + query, + // @ts-ignore - types are not up to date + params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], + }) + .toRecords(); +}; + +const buildDslFilter = ( + eventIds: string[], + showUnknownTarget: boolean, + start: string | number, + end: string | number, + esQuery?: EsQuery +) => ({ + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: start, + lte: end, + }, + }, + }, + ...(showUnknownTarget + ? [] + : [ { - range: { - '@timestamp': { - gte: start, - lte: end, - }, + exists: { + field: 'target.entity.id', }, }, + ]), + { + bool: { + should: [ + ...(esQuery?.bool.filter?.length || + esQuery?.bool.must?.length || + esQuery?.bool.should?.length || + esQuery?.bool.must_not?.length + ? [esQuery] + : []), { - bool: { - should: [ - { - terms: { - 'event.id': eventIds, - }, - }, - { - terms: { - 'actor.entity.id': actorIds, - }, - }, - ], - minimum_should_match: 1, + terms: { + 'event.id': eventIds, }, }, ], + minimum_should_match: 1, }, }, - query, - // @ts-ignore - types are not up to date - params: [...eventIds.map((id, idx) => ({ [`al_id${idx}`]: id }))], - }) - .toRecords(); -}; + ], + }, +}); -const createNodes = ( - logger: Logger, - records: GraphEdge[], - context: Omit -) => { +const createNodes = (records: GraphEdge[], context: Omit) => { const { nodesMap, edgeLabelsNodes, labelEdges } = context; for (const record of records) { + if (context.nodesLimit !== undefined && Object.keys(nodesMap).length >= context.nodesLimit) { + context.logger.debug( + `Reached nodes limit [limit: ${context.nodesLimit}] [current: ${ + Object.keys(nodesMap).length + }]` + ); + context.messages.push(ApiMessageCode.ReachedNodesLimit); + break; + } + const { ips, hosts, users, actorIds, action, targetIds, isAlert, eventOutcome } = record; const actorIdsArray = castArray(actorIds); const targetIdsArray = castArray(targetIds); @@ -190,12 +251,6 @@ const createNodes = ( } }); - logger.trace( - `Parsing record [actorIds: ${actorIdsArray.join( - ', ' - )}, action: ${action}, targetIds: ${targetIdsArray.join(', ')}]` - ); - // Create entity nodes [...actorIdsArray, ...targetIdsArray].forEach((id) => { if (nodesMap[id] === undefined) { @@ -203,10 +258,13 @@ const createNodes = ( id, label: unknownTargets.includes(id) ? 'Unknown' : undefined, color: isAlert ? 'danger' : 'primary', - ...determineEntityNodeShape(id, ips ?? [], hosts ?? [], users ?? []), + ...determineEntityNodeShape( + id, + castArray(ips ?? []), + castArray(hosts ?? []), + castArray(users ?? []) + ), }; - - logger.trace(`Creating entity node [${id}]`); } }); @@ -226,8 +284,6 @@ const createNodes = ( shape: 'label', }; - logger.trace(`Creating label node [${labelNode.id}]`); - nodesMap[labelNode.id] = labelNode; edgeLabelsNodes[edgeId].push(labelNode.id); labelEdges[labelNode.id] = { source: actorId, target: targetId }; @@ -278,7 +334,7 @@ const sortNodes = (nodesMap: Record) => { return [...groupNodes, ...otherNodes]; }; -const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { +const createEdgesAndGroups = (context: ParseContext) => { const { edgeLabelsNodes, edgesMap, nodesMap, labelEdges } = context; Object.entries(edgeLabelsNodes).forEach(([edgeId, edgeLabelsIds]) => { @@ -287,7 +343,6 @@ const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { const edgeLabelId = edgeLabelsIds[0]; connectEntitiesAndLabelNode( - logger, edgesMap, nodesMap, labelEdges[edgeLabelId].source, @@ -300,44 +355,47 @@ const createEdgesAndGroups = (logger: Logger, context: ParseContext) => { shape: 'group', }; nodesMap[groupNode.id] = groupNode; + let groupEdgesColor: Color = 'primary'; + + edgeLabelsIds.forEach((edgeLabelId) => { + (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; + connectEntitiesAndLabelNode(edgesMap, nodesMap, groupNode.id, edgeLabelId, groupNode.id); + + if ((nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'danger') { + groupEdgesColor = 'danger'; + } else if ( + (nodesMap[edgeLabelId] as LabelNodeDataModel).color === 'warning' && + groupEdgesColor !== 'danger' + ) { + // Use warning only if there's no danger color + groupEdgesColor = 'warning'; + } + }); connectEntitiesAndLabelNode( - logger, edgesMap, nodesMap, labelEdges[edgeLabelsIds[0]].source, groupNode.id, - labelEdges[edgeLabelsIds[0]].target + labelEdges[edgeLabelsIds[0]].target, + groupEdgesColor ); - - edgeLabelsIds.forEach((edgeLabelId) => { - (nodesMap[edgeLabelId] as Writable).parentId = groupNode.id; - connectEntitiesAndLabelNode( - logger, - edgesMap, - nodesMap, - groupNode.id, - edgeLabelId, - groupNode.id - ); - }); } }); }; const connectEntitiesAndLabelNode = ( - logger: Logger, edgesMap: Record, nodesMap: Record, sourceNodeId: string, labelNodeId: string, - targetNodeId: string + targetNodeId: string, + colorOverride?: Color ) => { [ - connectNodes(nodesMap, sourceNodeId, labelNodeId), - connectNodes(nodesMap, labelNodeId, targetNodeId), + connectNodes(nodesMap, sourceNodeId, labelNodeId, colorOverride), + connectNodes(nodesMap, labelNodeId, targetNodeId, colorOverride), ].forEach((edge) => { - logger.trace(`Connecting nodes [${edge.source} -> ${edge.target}]`); edgesMap[edge.id] = edge; }); }; @@ -345,7 +403,8 @@ const connectEntitiesAndLabelNode = ( const connectNodes = ( nodesMap: Record, sourceNodeId: string, - targetNodeId: string + targetNodeId: string, + colorOverride?: Color ): EdgeDataModel => { const sourceNode = nodesMap[sourceNodeId]; const targetNode = nodesMap[targetNodeId]; @@ -360,6 +419,6 @@ const connectNodes = ( id: `a(${sourceNodeId})-b(${targetNodeId})`, source: sourceNodeId, target: targetNodeId, - color, + color: colorOverride ?? color, }; }; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts index 0a3632efe9195..abdffd19eca76 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -34,7 +34,7 @@ export const transformElasticNamedSearchToListItem = ({ }: TransformElasticMSearchToListItemOptions): SearchListItemArraySchema => { return value.map((singleValue, index) => { const matchingHits = response.hits.hits.filter((hit) => { - if (hit.matched_queries != null) { + if (hit.matched_queries != null && Array.isArray(hit.matched_queries)) { return hit.matched_queries.some((matchedQuery) => { const [matchedQueryIndex] = matchedQuery.split('.'); return matchedQueryIndex === `${index}`; diff --git a/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx index e9db6b4e4f590..f7e95e3eda52c 100644 --- a/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx @@ -182,6 +182,7 @@ export const ExpandedRow: FC = ({ item }) => { key: `${perDeploymentStat.deployment_id}_${nodeName}`, ...perDeploymentStat, ...modelSizeStats, + // @ts-expect-error `throughput_last_minute` is not declared in ES Types node: { ...pick(n, [ 'average_inference_time_ms', diff --git a/x-pack/plugins/ml/server/models/model_management/memory_usage.ts b/x-pack/plugins/ml/server/models/model_management/memory_usage.ts index 6e2931dbbe06b..6e9121a133bb8 100644 --- a/x-pack/plugins/ml/server/models/model_management/memory_usage.ts +++ b/x-pack/plugins/ml/server/models/model_management/memory_usage.ts @@ -181,6 +181,7 @@ export class MemoryUsageService { const mlNodes = Object.entries(response.nodes).filter(([, node]) => node.roles.includes('ml')); + // @ts-expect-error `throughput_last_minute` is not declared in ES Types const nodeDeploymentStatsResponses: NodeDeploymentStatsResponse[] = mlNodes.map( ([nodeId, node]) => { const nodeFields = pick(node, NODE_FIELDS) as RequiredNodeFields; diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/exporting.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/exporting.md new file mode 100644 index 0000000000000..f4cbc66779a81 --- /dev/null +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/exporting.md @@ -0,0 +1,65 @@ +# Prebuilt Rule Export + +This is a test plan for the exporting of prebuilt rules. This feature is an aspect of `Milestone 2` of the [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) epic. + +Status: `in progress`. + +## Useful information + +### Tickets + +- [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) +- [Rule Exporting Feature](https://github.com/elastic/kibana/issues/180167#issue-2227974379) +- [Rule Export API PR](https://github.com/elastic/kibana/pull/194498) + +### Terminology + +- **prebuilt rule**: A rule contained in our `Prebuilt Security Detection Rules` integration in Fleet. +- **custom rule**: A rule defined by the user, which has no relation to the prebuilt rules +- **rule source, or ruleSource**: A field on the rule that defines the rule's categorization + +## Scenarios + +### Core Functionality + +#### Scenario: Exporting prebuilt rule individually +```Gherkin +Given a space with prebuilt rules installed +When the user selects "Export rule" from the "All actions" dropdown on the rule's page +Then the rule should be exported as an NDJSON file +And it should include an "immutable" field with a value of true +And its "ruleSource" "type" should be "external" +And its "ruleSource" "isCustomized" value should depend on whether the rule was customized +``` + +#### Scenario: Exporting prebuilt rules in bulk +```Gherkin +Given a space with prebuilt rules installed +When the user selects prebuilt rules in the alerts table +And chooses "Export" from bulk actions +Then the selected rules should be exported as an NDJSON file +And they should include an "immutable" field with a value of true +And their "ruleSource" "type" should be "external" +And their "ruleSource" "isCustomized" should depend on whether the rule was customized +``` + +#### Scenario: Exporting both prebuilt and custom rules in bulk +```Gherkin +Given a space with prebuilt and custom rules installed +When the user selects prebuilt rules in the alerts table +And chooses "Export" from bulk actions +Then the selected rules should be exported as an NDJSON file +And the prebuilt rules should include an "immutable" field with a value of true +And the custom rules should include an "immutable" field with a value of false +And the prebuilt rules' "ruleSource" "type" should be "external" +And the custom rules' "ruleSource" "type" should be "internal" +``` + +### Error Handling + +#### Scenario: Exporting beyond the export limit +```Gherkin +Given a space with prebuilt and custom rules installed +And the number of rules is greater than the export limit (defaults to 10_000) +Then the request should be rejected as a bad request +``` diff --git a/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/importing.md b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/importing.md new file mode 100644 index 0000000000000..0c947d0a52b95 --- /dev/null +++ b/x-pack/plugins/security_solution/docs/testing/test_plans/detection_response/prebuilt_rules/importing.md @@ -0,0 +1,127 @@ +# Prebuilt Rule Import + +This is a test plan for the importing of prebuilt rules. This feature is an aspect of `Milestone 2` of the [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) epic. + +Status: `in progress`. + +## Useful information + +### Tickets + +- [Rule Immutability/Customization](https://github.com/elastic/security-team/issues/1974) +- [Rule Importing Feature](https://github.com/elastic/kibana/issues/180168) +- [Rule Import API PR](https://github.com/elastic/kibana/pull/190198) + +### Terminology + +- **prebuilt rule**: A rule contained in our `Prebuilt Security Detection Rules` integration in Fleet. +- **custom rule**: A rule defined by the user, which has no relation to the prebuilt rules +- **rule source, or ruleSource**: A field on the rule that defines the rule's categorization + +## Scenarios + +### Core Functionality + +#### Scenario: Importing an unmodified prebuilt rule with a matching rule_id and version + +```Gherkin +Given the import payload contains a prebuilt rule with a matching rule_id and version, identical to the published rule +When the user imports the rule +Then the rule should be created or updated +And the ruleSource type should be "external" +And isCustomized should be false +``` + +#### Scenario: Importing a customized prebuilt rule with a matching rule_id and version + +```Gherkin +Given the import payload contains a prebuilt rule with a matching rule_id and version, modified from the published version +When the user imports the rule +Then the rule should be created or updated +And the ruleSource type should be "external" +And isCustomized should be true +``` + +#### Scenario: Importing a prebuilt rule with a matching rule_id but no matching version + +```Gherkin +Given the import payload contains a prebuilt rule with a matching rule_id but no matching version +When the user imports the rule +Then the rule should be created or updated +And the ruleSource type should be "external" +And isCustomized should be true +``` + +#### Scenario: Importing a prebuilt rule with a non-existent rule_id + +```Gherkin +Given the import payload contains a prebuilt rule with a non-existent rule_id +When the user imports the rule +Then the rule should be created +And the ruleSource type should be "internal" +``` + +#### Scenario: Importing a prebuilt rule without a rule_id field + +```Gherkin +Given the import payload contains a prebuilt rule without a rule_id field +When the user imports the rule +Then the import should be rejected with a message "rule_id field is required" +``` + +#### Scenario: Importing a prebuilt rule with a matching rule_id but missing a version field + +```Gherkin +Given the import payload contains a prebuilt rule without a version field +When the user imports the rule +Then the import should be rejected with a message "version field is required" +``` + +#### Scenario: Importing an existing custom rule missing a version field + +```Gherkin +Given the import payload contains an existing custom rule without a version field +When the user imports the rule +Then the rule should be updated +And the ruleSource type should be "internal" +And the "version" field should be set to the existing rule's "version" +``` + +#### Scenario: Importing a new custom rule missing a version field + +```Gherkin +Given the import payload contains a new custom rule without a version field +When the user imports the rule +Then the rule should be created +And the ruleSource type should be "internal" +And the "version" field should be set to 1 +``` + +#### Scenario: Importing a rule with overwrite flag set to true + +```Gherkin +Given the import payload contains a rule with an existing rule_id +And the overwrite flag is set to true +When the user imports the rule +Then the rule should be overwritten +And the ruleSource type should be calculated based on the rule_id and version +``` + +#### Scenario: Importing a rule with overwrite flag set to false + +```Gherkin +Given the import payload contains a rule with an existing rule_id +And the overwrite flag is set to false +When the user imports the rule +Then the import should be rejected with a message "rule_id already exists" +``` + +#### Scenario: Importing both custom and prebuilt rules + +```Gherkin +Given the import payload contains modified and unmodified, custom and prebuilt rules +When the user imports the rule +Then custom rules should be created or updated, with versions defaulted to 1 +And prebuilt rules should be created or updated, +And prebuilt rules missing versions should be rejected +``` diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index cc8f2abda9c4e..9bcf35fdb13ca 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -202,7 +202,9 @@ const onOpenTimeline = jest.fn(); const COMBO_BOX_TOGGLE_BUTTON_TEST_ID = 'comboBoxToggleListButton'; const VERSION_INPUT_TEST_ID = 'relatedIntegrationVersionDependency'; -describe('StepDefineRule', () => { +// Failing: See https://github.com/elastic/kibana/issues/199648 +// Failing: See https://github.com/elastic/kibana/issues/199700 +describe.skip('StepDefineRule', () => { beforeEach(() => { jest.clearAllMocks(); mockUseRuleFromTimeline.mockReturnValue({ onOpenTimeline, loading: false }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx index be65593364593..af9e8dca1f24f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/graph_preview_container.tsx @@ -32,7 +32,6 @@ export const GraphPreviewContainer: React.FC = () => { const graphFetchQuery = useFetchGraphData({ req: { query: { - actorIds: [], eventIds, start: DEFAULT_FROM, end: DEFAULT_TO, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx new file mode 100644 index 0000000000000..c22ec0caa82c5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useFetchGraphData } from './use_fetch_graph_data'; + +const mockUseQuery = jest.fn(); + +jest.mock('@tanstack/react-query', () => { + return { + useQuery: (...args: unknown[]) => mockUseQuery(...args), + }; +}); + +describe('useFetchGraphData', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should pass default options when options are not provided', () => { + renderHook(() => { + return useFetchGraphData({ + req: { + query: { + eventIds: [], + start: '2021-09-01T00:00:00.000Z', + end: '2021-09-01T23:59:59.999Z', + }, + }, + }); + }); + + expect(mockUseQuery.mock.calls).toHaveLength(1); + expect(mockUseQuery.mock.calls[0][2]).toEqual({ + enabled: true, + refetchOnWindowFocus: true, + }); + }); + + it('Should should not be enabled when enabled set to false', () => { + renderHook(() => { + return useFetchGraphData({ + req: { + query: { + eventIds: [], + start: '2021-09-01T00:00:00.000Z', + end: '2021-09-01T23:59:59.999Z', + }, + }, + options: { + enabled: false, + }, + }); + }); + + expect(mockUseQuery.mock.calls).toHaveLength(1); + expect(mockUseQuery.mock.calls[0][2]).toEqual({ + enabled: false, + refetchOnWindowFocus: true, + }); + }); + + it('Should should not be refetchOnWindowFocus when refetchOnWindowFocus set to false', () => { + renderHook(() => { + return useFetchGraphData({ + req: { + query: { + eventIds: [], + start: '2021-09-01T00:00:00.000Z', + end: '2021-09-01T23:59:59.999Z', + }, + }, + options: { + refetchOnWindowFocus: false, + }, + }); + }); + + expect(mockUseQuery.mock.calls).toHaveLength(1); + expect(mockUseQuery.mock.calls[0][2]).toEqual({ + enabled: true, + refetchOnWindowFocus: false, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts index 2304cfb8d4fd2..9a0e270a9b2e0 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_graph_data.ts @@ -10,6 +10,7 @@ import type { GraphRequest, GraphResponse, } from '@kbn/cloud-security-posture-common/types/graph/latest'; +import { useMemo } from 'react'; import { EVENT_GRAPH_VISUALIZATION_API } from '../../../../../common/constants'; import { useHttp } from '../../../../common/lib/kibana'; @@ -30,6 +31,11 @@ export interface UseFetchGraphDataParams { * Defaults to true. */ enabled?: boolean; + /** + * If true, the query will refetch on window focus. + * Defaults to true. + */ + refetchOnWindowFocus?: boolean; }; } @@ -61,18 +67,25 @@ export const useFetchGraphData = ({ req, options, }: UseFetchGraphDataParams): UseFetchGraphDataResult => { - const { actorIds, eventIds, start, end } = req.query; + const { eventIds, start, end, esQuery } = req.query; const http = useHttp(); + const QUERY_KEY = useMemo( + () => ['useFetchGraphData', eventIds, start, end, esQuery], + [end, esQuery, eventIds, start] + ); const { isLoading, isError, data } = useQuery( - ['useFetchGraphData', actorIds, eventIds, start, end], + QUERY_KEY, () => { return http.post(EVENT_GRAPH_VISUALIZATION_API, { version: '1', body: JSON.stringify(req), }); }, - options + { + enabled: options?.enabled ?? true, + refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true, + } ); return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts index 8f98eab1a93e9..0d9882fe8aec3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/enrich_signal_threat_matches.ts @@ -24,8 +24,10 @@ export const groupAndMergeSignalMatches = (signalHits: SignalSourceHit[]): Signa if (existingSignalHit == null) { acc[signalId] = signalHit; } else { - const existingQueries = existingSignalHit?.matched_queries ?? []; - const newQueries = signalHit.matched_queries ?? []; + const existingQueries = Array.isArray(existingSignalHit?.matched_queries) + ? existingSignalHit.matched_queries + : []; + const newQueries = Array.isArray(signalHit.matched_queries) ? signalHit.matched_queries : []; existingSignalHit.matched_queries = [...existingQueries, ...newQueries]; acc[signalId] = existingSignalHit; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts index 309516a57335c..9694d37aab0ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/get_signals_map_from_threat_index.ts @@ -90,7 +90,9 @@ export async function getSignalsQueryMapFromThreatIndex( while (maxThreatsReachedMap.size < eventsCount && threatList?.hits.hits.length > 0) { threatList.hits.hits.forEach((threatHit) => { - const matchedQueries = threatHit?.matched_queries || []; + const matchedQueries = Array.isArray(threatHit?.matched_queries) + ? threatHit.matched_queries + : []; matchedQueries.forEach((matchedQuery) => { const decodedQuery = decodeThreatMatchNamedQuery(matchedQuery); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts index da72d121c371c..347ea5d1d94c6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/threat_mapping/utils.ts @@ -189,7 +189,9 @@ export const decodeThreatMatchNamedQuery = (encoded: string): DecodedThreatNamed export const extractNamedQueries = ( hit: SignalSourceHit | ThreatListItem ): DecodedThreatNamedQuery[] => - hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? []; + Array.isArray(hit.matched_queries) + ? hit.matched_queries.map((match) => decodeThreatMatchNamedQuery(match)) + : []; export const buildExecutionIntervalValidator: (interval: string) => () => void = (interval) => { const intervalDuration = parseInterval(interval); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts index e15aceb8a713b..54af298d11a3e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/event_enrichment/helpers.ts @@ -46,16 +46,19 @@ export const buildIndicatorShouldClauses = ( export const buildIndicatorEnrichments = (hits: estypes.SearchHit[]): CtiEnrichment[] => { return hits.flatMap(({ matched_queries: matchedQueries, ...hit }) => { return ( - matchedQueries?.reduce((enrichments, matchedQuery) => { - if (isValidEventField(matchedQuery)) { - enrichments.push({ - ...hit.fields, - ...buildIndicatorMatchedFields(hit, matchedQuery), - }); - } + (Array.isArray(matchedQueries) ? matchedQueries : [])?.reduce( + (enrichments, matchedQuery) => { + if (isValidEventField(matchedQuery)) { + enrichments.push({ + ...hit.fields, + ...buildIndicatorMatchedFields(hit, matchedQuery), + }); + } - return enrichments; - }, []) ?? [] + return enrichments; + }, + [] + ) ?? [] ); }); }; diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 45960195be216..cd820d1e70780 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -29,7 +29,7 @@ import { TaskManagerConfig } from './config'; import { createInitialMiddleware, addMiddlewareToChain, Middleware } from './lib/middleware'; import { removeIfExists } from './lib/remove_if_exists'; import { setupSavedObjects, BACKGROUND_TASK_NODE_SO_NAME, TASK_SO_NAME } from './saved_objects'; -import { TaskDefinitionRegistry, TaskTypeDictionary, REMOVED_TYPES } from './task_type_dictionary'; +import { TaskDefinitionRegistry, TaskTypeDictionary } from './task_type_dictionary'; import { AggregationOpts, FetchResult, SearchOpts, TaskStore } from './task_store'; import { createManagedConfiguration } from './lib/create_managed_configuration'; import { TaskScheduling } from './task_scheduling'; @@ -45,6 +45,10 @@ import { metricsStream, Metrics } from './metrics'; import { TaskManagerMetricsCollector } from './metrics/task_metrics_collector'; import { TaskPartitioner } from './lib/task_partitioner'; import { getDefaultCapacity } from './lib/get_default_capacity'; +import { + registerMarkRemovedTasksAsUnrecognizedDefinition, + scheduleMarkRemovedTasksAsUnrecognizedDefinition, +} from './removed_tasks/mark_removed_tasks_as_unrecognized'; export interface TaskManagerSetupContract { /** @@ -221,6 +225,11 @@ export class TaskManagerPlugin } registerDeleteInactiveNodesTaskDefinition(this.logger, core.getStartServices, this.definitions); + registerMarkRemovedTasksAsUnrecognizedDefinition( + this.logger, + core.getStartServices, + this.definitions + ); if (this.config.unsafe.exclude_task_types.length) { this.logger.warn( @@ -332,7 +341,6 @@ export class TaskManagerPlugin this.taskPollingLifecycle = new TaskPollingLifecycle({ config: this.config!, definitions: this.definitions, - unusedTypes: REMOVED_TYPES, logger: this.logger, executionContext, taskStore, @@ -384,6 +392,7 @@ export class TaskManagerPlugin }); scheduleDeleteInactiveNodesTaskDefinition(this.logger, taskScheduling).catch(() => {}); + scheduleMarkRemovedTasksAsUnrecognizedDefinition(this.logger, taskScheduling).catch(() => {}); return { fetch: (opts: SearchOpts): Promise => taskStore.fetch(opts), diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index 1f244f7f4c8a5..a408bd3f634d9 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -106,7 +106,6 @@ describe('TaskPollingLifecycle', () => { }, taskStore: mockTaskStore, logger: taskManagerLogger, - unusedTypes: [], definitions: new TaskTypeDictionary(taskManagerLogger), middleware: createInitialMiddleware(), startingCapacity: 20, diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index 0b1710ae7fa2f..fb6776fa34f28 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -55,7 +55,6 @@ export interface ITaskEventEmitter { export type TaskPollingLifecycleOpts = { logger: Logger; definitions: TaskTypeDictionary; - unusedTypes: string[]; taskStore: TaskStore; config: TaskManagerConfig; middleware: Middleware; @@ -115,7 +114,6 @@ export class TaskPollingLifecycle implements ITaskEventEmitter this.pool.availableCapacity(taskType), taskPartitioner, diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts index 76df8b7ae5584..fa1d1f749985b 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.test.ts @@ -70,7 +70,6 @@ describe('mark_available_tasks_as_claimed', () => { fieldUpdates, claimableTaskTypes: definitions.getAllTypes(), skippedTaskTypes: [], - unusedTaskTypes: [], taskMaxAttempts: Array.from(definitions).reduce((accumulator, [type, { maxAttempts }]) => { return { ...accumulator, [type]: maxAttempts || defaultMaxAttempts }; }, {}), @@ -153,8 +152,6 @@ if (doc['task.runAt'].size()!=0) { ctx._source.task.status = "claiming"; ${Object.keys(fieldUpdates) .map((field) => `ctx._source.task.${field}=params.fieldUpdates.${field};`) .join(' ')} - } else if (params.unusedTaskTypes.contains(ctx._source.task.taskType)) { - ctx._source.task.status = "unrecognized"; } else { ctx.op = "noop"; }`, @@ -167,7 +164,6 @@ if (doc['task.runAt'].size()!=0) { }, claimableTaskTypes: ['sampleTask', 'otherTask'], skippedTaskTypes: [], - unusedTaskTypes: [], taskMaxAttempts: { sampleTask: 5, otherTask: 1, @@ -242,7 +238,6 @@ if (doc['task.runAt'].size()!=0) { fieldUpdates, claimableTaskTypes: ['foo', 'bar'], skippedTaskTypes: [], - unusedTaskTypes: [], taskMaxAttempts: { foo: 5, bar: 2, diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 4e138545aec25..ec99c6ad5bf80 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -202,7 +202,6 @@ export interface UpdateFieldsAndMarkAsFailedOpts { }; claimableTaskTypes: string[]; skippedTaskTypes: string[]; - unusedTaskTypes: string[]; taskMaxAttempts: { [field: string]: number }; } @@ -210,7 +209,6 @@ export const updateFieldsAndMarkAsFailed = ({ fieldUpdates, claimableTaskTypes, skippedTaskTypes, - unusedTaskTypes, taskMaxAttempts, }: UpdateFieldsAndMarkAsFailedOpts): ScriptClause => { const setScheduledAtScript = `if(ctx._source.task.retryAt != null && ZonedDateTime.parse(ctx._source.task.retryAt).toInstant().toEpochMilli() < params.now) { @@ -227,8 +225,6 @@ export const updateFieldsAndMarkAsFailed = ({ source: ` if (params.claimableTaskTypes.contains(ctx._source.task.taskType)) { ${setScheduledAtAndMarkAsClaimed} - } else if (params.unusedTaskTypes.contains(ctx._source.task.taskType)) { - ctx._source.task.status = "unrecognized"; } else { ctx.op = "noop"; }`, @@ -238,7 +234,6 @@ export const updateFieldsAndMarkAsFailed = ({ fieldUpdates, claimableTaskTypes, skippedTaskTypes, - unusedTaskTypes, taskMaxAttempts, }, }; diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts index 437af8e007bdb..629e3464399c7 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.test.ts @@ -83,7 +83,6 @@ describe('TaskClaiming', () => { strategy: 'non-default', definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore: taskStoreMock.create({ taskManagerId: '' }), maxAttempts: 2, getAvailableCapacity: () => 10, @@ -134,7 +133,6 @@ describe('TaskClaiming', () => { strategy: 'default', definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore: taskStoreMock.create({ taskManagerId: '' }), maxAttempts: 2, getAvailableCapacity: () => 10, diff --git a/x-pack/plugins/task_manager/server/queries/task_claiming.ts b/x-pack/plugins/task_manager/server/queries/task_claiming.ts index c9bca31755408..1b1e414903628 100644 --- a/x-pack/plugins/task_manager/server/queries/task_claiming.ts +++ b/x-pack/plugins/task_manager/server/queries/task_claiming.ts @@ -34,7 +34,6 @@ export interface TaskClaimingOpts { logger: Logger; strategy: string; definitions: TaskTypeDictionary; - unusedTypes: string[]; taskStore: TaskStore; maxAttempts: number; excludedTaskTypes: string[]; @@ -92,7 +91,6 @@ export class TaskClaiming { private readonly taskClaimingBatchesByType: TaskClaimingBatches; private readonly taskMaxAttempts: Record; private readonly excludedTaskTypes: string[]; - private readonly unusedTypes: string[]; private readonly taskClaimer: TaskClaimerFn; private readonly taskPartitioner: TaskPartitioner; @@ -111,7 +109,6 @@ export class TaskClaiming { this.taskClaimingBatchesByType = this.partitionIntoClaimingBatches(this.definitions); this.taskMaxAttempts = Object.fromEntries(this.normalizeMaxAttempts(this.definitions)); this.excludedTaskTypes = opts.excludedTaskTypes; - this.unusedTypes = opts.unusedTypes; this.taskClaimer = getTaskClaimer(this.logger, opts.strategy); this.events$ = new Subject(); this.taskPartitioner = opts.taskPartitioner; @@ -178,7 +175,6 @@ export class TaskClaiming { taskStore: this.taskStore, events$: this.events$, getCapacity: this.getAvailableCapacity, - unusedTypes: this.unusedTypes, definitions: this.definitions, taskMaxAttempts: this.taskMaxAttempts, excludedTaskTypes: this.excludedTaskTypes, diff --git a/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.test.ts b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.test.ts new file mode 100644 index 0000000000000..1485216a67f33 --- /dev/null +++ b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.test.ts @@ -0,0 +1,266 @@ +/* + * 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 { mockLogger } from '../test_utils'; +import { coreMock, elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { SCHEDULE_INTERVAL, taskRunner } from './mark_removed_tasks_as_unrecognized'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +const createTaskDoc = (id: string = '1'): SearchHit => ({ + _index: '.kibana_task_manager_9.0.0_001', + _id: `task:${id}`, + _score: 1, + _source: { + references: [], + type: 'task', + updated_at: '2024-11-06T14:17:55.935Z', + task: { + taskType: 'report', + params: '{}', + state: '{"foo":"test"}', + stateVersion: 1, + runAt: '2024-11-06T14:17:55.935Z', + enabled: true, + scheduledAt: '2024-11-06T14:17:55.935Z', + attempts: 0, + status: 'idle', + startedAt: null, + retryAt: null, + ownerId: null, + partition: 211, + }, + }, +}); + +describe('markRemovedTasksAsUnrecognizedTask', () => { + const logger = mockLogger(); + const coreSetup = coreMock.createSetup(); + const esClient = elasticsearchServiceMock.createStart(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('marks removed tasks as unrecognized', async () => { + esClient.client.asInternalUser.bulk.mockResolvedValue({ + errors: false, + took: 0, + items: [ + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:123', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:456', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:789', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + ], + }); + + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + // @ts-expect-error + esClient.client.asInternalUser.search.mockResponse({ + hits: { hits: [createTaskDoc('123'), createTaskDoc('456'), createTaskDoc('789')], total: 3 }, + }); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).toHaveBeenCalledWith({ + body: [ + { update: { _id: 'task:123' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:456' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:789' } }, + { doc: { task: { status: 'unrecognized' } } }, + ], + index: '.kibana_task_manager', + refresh: false, + }); + + expect(logger.debug).toHaveBeenCalledWith(`Marked 3 removed tasks as unrecognized`); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); + + it('skips update when there are no removed task types', async () => { + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + // @ts-expect-error + esClient.client.asInternalUser.search.mockResponse({ + hits: { hits: [], total: 0 }, + }); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).not.toHaveBeenCalled(); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); + + it('schedules the next run even when there is an error', async () => { + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + esClient.client.asInternalUser.search.mockRejectedValueOnce(new Error('foo')); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).not.toHaveBeenCalled(); + + expect(logger.error).toHaveBeenCalledWith( + 'Failed to mark removed tasks as unrecognized. Error: foo' + ); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); + + it('handles partial errors from bulk partial update', async () => { + esClient.client.asInternalUser.bulk.mockResolvedValue({ + errors: false, + took: 0, + items: [ + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:123', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:456', + _version: 2, + result: 'updated', + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 84, + _primary_term: 1, + status: 200, + }, + }, + { + update: { + _index: '.kibana_task_manager_9.0.0_001', + _id: 'task:789', + _version: 2, + error: { + type: 'document_missing_exception', + reason: '[5]: document missing', + index_uuid: 'aAsFqTI0Tc2W0LCWgPNrOA', + shard: '0', + index: '.kibana_task_manager_9.0.0_001', + }, + status: 404, + }, + }, + ], + }); + + coreSetup.getStartServices.mockResolvedValue([ + { + ...coreMock.createStart(), + elasticsearch: esClient, + }, + {}, + coreMock.createSetup(), + ]); + // @ts-expect-error + esClient.client.asInternalUser.search.mockResponse({ + hits: { hits: [createTaskDoc('123'), createTaskDoc('456'), createTaskDoc('789')], total: 3 }, + }); + + const runner = taskRunner(logger, coreSetup.getStartServices)(); + const result = await runner.run(); + + expect(esClient.client.asInternalUser.bulk).toHaveBeenCalledWith({ + body: [ + { update: { _id: 'task:123' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:456' } }, + { doc: { task: { status: 'unrecognized' } } }, + { update: { _id: 'task:789' } }, + { doc: { task: { status: 'unrecognized' } } }, + ], + index: '.kibana_task_manager', + refresh: false, + }); + expect(logger.warn).toHaveBeenCalledWith( + `Error updating task task:789 to mark as unrecognized - {\"type\":\"document_missing_exception\",\"reason\":\"[5]: document missing\",\"index_uuid\":\"aAsFqTI0Tc2W0LCWgPNrOA\",\"shard\":\"0\",\"index\":\".kibana_task_manager_9.0.0_001\"}` + ); + + expect(logger.debug).toHaveBeenCalledWith(`Marked 2 removed tasks as unrecognized`); + + expect(result).toEqual({ + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.ts b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.ts new file mode 100644 index 0000000000000..e28d5221e72d5 --- /dev/null +++ b/x-pack/plugins/task_manager/server/removed_tasks/mark_removed_tasks_as_unrecognized.ts @@ -0,0 +1,150 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import { CoreStart } from '@kbn/core-lifecycle-server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { TaskScheduling } from '../task_scheduling'; +import { TaskTypeDictionary } from '../task_type_dictionary'; +import { ConcreteTaskInstance, TaskManagerStartContract } from '..'; +import { TaskStatus } from '../task'; +import { REMOVED_TYPES } from '../task_type_dictionary'; +import { TASK_MANAGER_INDEX } from '../constants'; + +export const TASK_ID = 'mark_removed_tasks_as_unrecognized'; +const TASK_TYPE = `task_manager:${TASK_ID}`; + +export const SCHEDULE_INTERVAL = '1h'; + +export async function scheduleMarkRemovedTasksAsUnrecognizedDefinition( + logger: Logger, + taskScheduling: TaskScheduling +) { + try { + await taskScheduling.ensureScheduled({ + id: TASK_ID, + taskType: TASK_TYPE, + schedule: { interval: SCHEDULE_INTERVAL }, + state: {}, + params: {}, + }); + } catch (e) { + logger.error(`Error scheduling ${TASK_ID} task, received ${e.message}`); + } +} + +export function registerMarkRemovedTasksAsUnrecognizedDefinition( + logger: Logger, + coreStartServices: () => Promise<[CoreStart, TaskManagerStartContract, unknown]>, + taskTypeDictionary: TaskTypeDictionary +) { + taskTypeDictionary.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Mark removed tasks as unrecognized', + createTaskRunner: taskRunner(logger, coreStartServices), + }, + }); +} + +export function taskRunner( + logger: Logger, + coreStartServices: () => Promise<[CoreStart, TaskManagerStartContract, unknown]> +) { + return () => { + return { + async run() { + try { + const [{ elasticsearch }] = await coreStartServices(); + const esClient = elasticsearch.client.asInternalUser; + + const removedTasks = await queryForRemovedTasks(esClient); + + if (removedTasks.length > 0) { + await updateTasksToBeUnrecognized(esClient, logger, removedTasks); + } + + return { + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }; + } catch (e) { + logger.error(`Failed to mark removed tasks as unrecognized. Error: ${e.message}`); + return { + state: {}, + schedule: { interval: SCHEDULE_INTERVAL }, + }; + } + }, + }; + }; +} + +async function queryForRemovedTasks( + esClient: ElasticsearchClient +): Promise>> { + const result = await esClient.search({ + index: TASK_MANAGER_INDEX, + body: { + size: 100, + _source: false, + query: { + bool: { + must: [ + { + terms: { + 'task.taskType': REMOVED_TYPES, + }, + }, + ], + }, + }, + }, + }); + + return result.hits.hits; +} + +async function updateTasksToBeUnrecognized( + esClient: ElasticsearchClient, + logger: Logger, + removedTasks: Array> +) { + const bulkBody = []; + for (const task of removedTasks) { + bulkBody.push({ update: { _id: task._id } }); + bulkBody.push({ doc: { task: { status: TaskStatus.Unrecognized } } }); + } + + let removedCount = 0; + try { + const removeResults = await esClient.bulk({ + index: TASK_MANAGER_INDEX, + refresh: false, + body: bulkBody, + }); + for (const removeResult of removeResults.items) { + if (!removeResult.update || !removeResult.update._id) { + logger.warn( + `Error updating task with unknown to mark as unrecognized - malformed response` + ); + } else if (removeResult.update?.error) { + logger.warn( + `Error updating task ${ + removeResult.update._id + } to mark as unrecognized - ${JSON.stringify(removeResult.update.error)}` + ); + } else { + removedCount++; + } + } + logger.debug(`Marked ${removedCount} removed tasks as unrecognized`); + } catch (err) { + // don't worry too much about errors, we'll try again next time + logger.warn(`Error updating tasks to mark as unrecognized: ${err}`); + } +} diff --git a/x-pack/plugins/task_manager/server/task_claimers/index.ts b/x-pack/plugins/task_manager/server/task_claimers/index.ts index 178ebacf68cb9..f41c489fd7550 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/index.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/index.ts @@ -26,7 +26,6 @@ export interface TaskClaimerOpts { events$: Subject; taskStore: TaskStore; definitions: TaskTypeDictionary; - unusedTypes: string[]; excludedTaskTypes: string[]; taskMaxAttempts: Record; logger: Logger; diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts index fe44ce9e94c68..07dae3c48a392 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.test.ts @@ -190,7 +190,6 @@ describe('TaskClaiming', () => { definitions, taskStore: store, excludedTaskTypes, - unusedTypes: unusedTaskTypes, maxAttempts: taskClaimingOpts.maxAttempts ?? 2, getAvailableCapacity: taskClaimingOpts.getAvailableCapacity ?? (() => 10), taskPartitioner, @@ -206,20 +205,17 @@ describe('TaskClaiming', () => { claimingOpts, hits = [generateFakeTasks(1)], excludedTaskTypes = [], - unusedTaskTypes = [], }: { storeOpts: Partial; taskClaimingOpts: Partial; claimingOpts: Omit; hits?: ConcreteTaskInstance[][]; excludedTaskTypes?: string[]; - unusedTaskTypes?: string[]; }) { const { taskClaiming, store } = initialiseTestClaiming({ storeOpts, taskClaimingOpts, excludedTaskTypes, - unusedTaskTypes, hits, }); @@ -355,7 +351,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -378,7 +373,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 3; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 3; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -440,312 +435,6 @@ describe('TaskClaiming', () => { expect(result.docs.length).toEqual(3); }); - test('should not claim tasks of removed type', async () => { - const store = taskStoreMock.create({ taskManagerId: 'test-test' }); - store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); - - const fetchedTasks = [ - mockInstance({ id: `id-1`, taskType: 'report' }), - mockInstance({ id: `id-2`, taskType: 'report' }), - mockInstance({ id: `id-3`, taskType: 'yawn' }), - ]; - - const { versionMap, docLatestVersions } = getVersionMapsFromTasks(fetchedTasks); - store.msearch.mockResolvedValueOnce({ docs: fetchedTasks, versionMap }); - store.getDocVersions.mockResolvedValueOnce(docLatestVersions); - - store.bulkGet.mockResolvedValueOnce([fetchedTasks[2]].map(asOk)); - store.bulkPartialUpdate.mockResolvedValueOnce([fetchedTasks[2]].map(getPartialUpdateResult)); - store.bulkPartialUpdate.mockResolvedValueOnce( - [fetchedTasks[0], fetchedTasks[1]].map(getPartialUpdateResult) - ); - - const taskClaiming = new TaskClaiming({ - logger: taskManagerLogger, - strategy: CLAIM_STRATEGY_MGET, - definitions: taskDefinitions, - taskStore: store, - excludedTaskTypes: [], - unusedTypes: ['report'], - maxAttempts: 2, - getAvailableCapacity: () => 10, - taskPartitioner, - }); - - const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ - claimOwnershipUntil: new Date(), - }); - - if (!isOk(resultOrErr)) { - expect(resultOrErr).toBe(undefined); - } - - const result = unwrap(resultOrErr) as ClaimOwnershipResult; - - expect(apm.startTransaction).toHaveBeenCalledWith( - TASK_MANAGER_MARK_AS_CLAIMED, - TASK_MANAGER_TRANSACTION_TYPE - ); - expect(mockApmTrans.end).toHaveBeenCalledWith('success'); - - expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 1; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 2;', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - - expect(store.msearch.mock.calls[0][0]?.[0]).toMatchObject({ - size: 40, - seq_no_primary_term: true, - }); - expect(store.getDocVersions).toHaveBeenCalledWith(['task:id-1', 'task:id-2', 'task:id-3']); - expect(store.bulkPartialUpdate).toHaveBeenCalledTimes(2); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(1, [ - { - id: fetchedTasks[2].id, - version: fetchedTasks[2].version, - scheduledAt: fetchedTasks[2].runAt, - attempts: 1, - ownerId: 'test-test', - retryAt: new Date('1970-01-01T00:05:30.000Z'), - status: 'running', - startedAt: new Date('1970-01-01T00:00:00.000Z'), - }, - ]); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(2, [ - { - id: fetchedTasks[0].id, - version: fetchedTasks[0].version, - status: 'unrecognized', - }, - { - id: fetchedTasks[1].id, - version: fetchedTasks[1].version, - status: 'unrecognized', - }, - ]); - expect(store.bulkGet).toHaveBeenCalledWith(['id-3']); - - expect(result.stats).toEqual({ - tasksClaimed: 1, - tasksConflicted: 0, - tasksErrors: 0, - tasksUpdated: 1, - tasksLeftUnclaimed: 0, - staleTasks: 0, - }); - expect(result.docs.length).toEqual(1); - }); - - test('should log warning if error updating single removed task as unrecognized', async () => { - const store = taskStoreMock.create({ taskManagerId: 'test-test' }); - store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); - - const fetchedTasks = [ - mockInstance({ id: `id-1`, taskType: 'report' }), - mockInstance({ id: `id-2`, taskType: 'report' }), - mockInstance({ id: `id-3`, taskType: 'yawn' }), - ]; - - const { versionMap, docLatestVersions } = getVersionMapsFromTasks(fetchedTasks); - store.msearch.mockResolvedValueOnce({ docs: fetchedTasks, versionMap }); - store.getDocVersions.mockResolvedValueOnce(docLatestVersions); - - store.bulkGet.mockResolvedValueOnce([fetchedTasks[2]].map(asOk)); - store.bulkPartialUpdate.mockResolvedValueOnce([fetchedTasks[2]].map(getPartialUpdateResult)); - store.bulkPartialUpdate.mockResolvedValueOnce([ - asOk(fetchedTasks[0]), - asErr({ - type: 'task', - id: fetchedTasks[1].id, - status: 404, - error: { - type: 'document_missing_exception', - reason: '[5]: document missing', - index_uuid: 'aAsFqTI0Tc2W0LCWgPNrOA', - shard: '0', - index: '.kibana_task_manager_8.16.0_001', - }, - }), - ]); - - const taskClaiming = new TaskClaiming({ - logger: taskManagerLogger, - strategy: CLAIM_STRATEGY_MGET, - definitions: taskDefinitions, - taskStore: store, - excludedTaskTypes: [], - unusedTypes: ['report'], - maxAttempts: 2, - getAvailableCapacity: () => 10, - taskPartitioner, - }); - - const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ - claimOwnershipUntil: new Date(), - }); - - if (!isOk(resultOrErr)) { - expect(resultOrErr).toBe(undefined); - } - - const result = unwrap(resultOrErr) as ClaimOwnershipResult; - - expect(apm.startTransaction).toHaveBeenCalledWith( - TASK_MANAGER_MARK_AS_CLAIMED, - TASK_MANAGER_TRANSACTION_TYPE - ); - expect(mockApmTrans.end).toHaveBeenCalledWith('success'); - - expect(taskManagerLogger.warn).toHaveBeenCalledWith( - 'Error updating task id-2:task to mark as unrecognized during claim: {"type":"document_missing_exception","reason":"[5]: document missing","index_uuid":"aAsFqTI0Tc2W0LCWgPNrOA","shard":"0","index":".kibana_task_manager_8.16.0_001"}', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 1; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 1;', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - - expect(store.msearch.mock.calls[0][0]?.[0]).toMatchObject({ - size: 40, - seq_no_primary_term: true, - }); - expect(store.getDocVersions).toHaveBeenCalledWith(['task:id-1', 'task:id-2', 'task:id-3']); - expect(store.bulkPartialUpdate).toHaveBeenCalledTimes(2); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(1, [ - { - id: fetchedTasks[2].id, - version: fetchedTasks[2].version, - scheduledAt: fetchedTasks[2].runAt, - attempts: 1, - ownerId: 'test-test', - retryAt: new Date('1970-01-01T00:05:30.000Z'), - status: 'running', - startedAt: new Date('1970-01-01T00:00:00.000Z'), - }, - ]); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(2, [ - { - id: fetchedTasks[0].id, - version: fetchedTasks[0].version, - status: 'unrecognized', - }, - { - id: fetchedTasks[1].id, - version: fetchedTasks[1].version, - status: 'unrecognized', - }, - ]); - expect(store.bulkGet).toHaveBeenCalledWith(['id-3']); - - expect(result.stats).toEqual({ - tasksClaimed: 1, - tasksConflicted: 0, - tasksErrors: 0, - tasksUpdated: 1, - tasksLeftUnclaimed: 0, - staleTasks: 0, - }); - expect(result.docs.length).toEqual(1); - }); - - test('should log warning if error updating all removed tasks as unrecognized', async () => { - const store = taskStoreMock.create({ taskManagerId: 'test-test' }); - store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); - - const fetchedTasks = [ - mockInstance({ id: `id-1`, taskType: 'report' }), - mockInstance({ id: `id-2`, taskType: 'report' }), - mockInstance({ id: `id-3`, taskType: 'yawn' }), - ]; - - const { versionMap, docLatestVersions } = getVersionMapsFromTasks(fetchedTasks); - store.msearch.mockResolvedValueOnce({ docs: fetchedTasks, versionMap }); - store.getDocVersions.mockResolvedValueOnce(docLatestVersions); - - store.bulkGet.mockResolvedValueOnce([fetchedTasks[2]].map(asOk)); - store.bulkPartialUpdate.mockResolvedValueOnce([fetchedTasks[2]].map(getPartialUpdateResult)); - store.bulkPartialUpdate.mockRejectedValueOnce(new Error('Oh no')); - - const taskClaiming = new TaskClaiming({ - logger: taskManagerLogger, - strategy: CLAIM_STRATEGY_MGET, - definitions: taskDefinitions, - taskStore: store, - excludedTaskTypes: [], - unusedTypes: ['report'], - maxAttempts: 2, - getAvailableCapacity: () => 10, - taskPartitioner, - }); - - const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ - claimOwnershipUntil: new Date(), - }); - - if (!isOk(resultOrErr)) { - expect(resultOrErr).toBe(undefined); - } - - const result = unwrap(resultOrErr) as ClaimOwnershipResult; - - expect(apm.startTransaction).toHaveBeenCalledWith( - TASK_MANAGER_MARK_AS_CLAIMED, - TASK_MANAGER_TRANSACTION_TYPE - ); - expect(mockApmTrans.end).toHaveBeenCalledWith('success'); - - expect(taskManagerLogger.warn).toHaveBeenCalledWith( - 'Error updating tasks to mark as unrecognized during claim: Error: Oh no', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 1; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', - { tags: ['taskClaiming', 'claimAvailableTasksMget'] } - ); - - expect(store.msearch.mock.calls[0][0]?.[0]).toMatchObject({ - size: 40, - seq_no_primary_term: true, - }); - expect(store.getDocVersions).toHaveBeenCalledWith(['task:id-1', 'task:id-2', 'task:id-3']); - expect(store.bulkGet).toHaveBeenCalledWith(['id-3']); - expect(store.bulkPartialUpdate).toHaveBeenCalledTimes(2); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(1, [ - { - id: fetchedTasks[2].id, - version: fetchedTasks[2].version, - scheduledAt: fetchedTasks[2].runAt, - attempts: 1, - ownerId: 'test-test', - retryAt: new Date('1970-01-01T00:05:30.000Z'), - status: 'running', - startedAt: new Date('1970-01-01T00:00:00.000Z'), - }, - ]); - expect(store.bulkPartialUpdate).toHaveBeenNthCalledWith(2, [ - { - id: fetchedTasks[0].id, - version: fetchedTasks[0].version, - status: 'unrecognized', - }, - { - id: fetchedTasks[1].id, - version: fetchedTasks[1].version, - status: 'unrecognized', - }, - ]); - - expect(result.stats).toEqual({ - tasksClaimed: 1, - tasksConflicted: 0, - tasksErrors: 0, - tasksUpdated: 1, - tasksLeftUnclaimed: 0, - staleTasks: 0, - }); - expect(result.docs.length).toEqual(1); - }); - test('should handle no tasks to claim', async () => { const store = taskStoreMock.create({ taskManagerId: 'test-test' }); store.convertToSavedObjectIds.mockImplementation((ids) => ids.map((id) => `task:${id}`)); @@ -761,7 +450,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -828,7 +516,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -851,7 +538,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -922,7 +609,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -945,7 +631,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 2; stale: 0; conflicts: 0; missing: 1; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -1016,7 +702,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1039,7 +724,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 2; stale: 1; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 2; stale: 1; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -1116,7 +801,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1139,7 +823,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 4; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 4; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); @@ -1248,7 +932,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1271,7 +954,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 1; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 1;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.error).toHaveBeenCalledWith( @@ -1377,7 +1060,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1400,7 +1082,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.warn).toHaveBeenCalledWith( @@ -1504,7 +1186,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1619,7 +1300,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1642,7 +1322,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 1; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 0; missing: 0; capacity reached: 0; updateErrors: 1; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.error).toHaveBeenCalledWith( @@ -1753,7 +1433,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -1776,7 +1455,7 @@ describe('TaskClaiming', () => { expect(mockApmTrans.end).toHaveBeenCalledWith('success'); expect(taskManagerLogger.debug).toHaveBeenCalledWith( - 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0; removed: 0;', + 'task claimer claimed: 3; stale: 0; conflicts: 1; missing: 0; capacity reached: 0; updateErrors: 0; getErrors: 0;', { tags: ['taskClaiming', 'claimAvailableTasksMget'] } ); expect(taskManagerLogger.error).not.toHaveBeenCalled(); @@ -1870,7 +1549,6 @@ describe('TaskClaiming', () => { definitions: taskDefinitions, taskStore: store, excludedTaskTypes: [], - unusedTypes: [], maxAttempts: 2, getAvailableCapacity: () => 10, taskPartitioner, @@ -2488,7 +2166,6 @@ describe('TaskClaiming', () => { strategy: CLAIM_STRATEGY_MGET, definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore, maxAttempts: 2, getAvailableCapacity, diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts index 16d9ba5c7fae7..431daab8dd2cb 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_mget.ts @@ -57,7 +57,6 @@ interface OwnershipClaimingOpts { claimOwnershipUntil: Date; size: number; taskTypes: Set; - removedTypes: Set; getCapacity: (taskType?: string | undefined) => number; excludedTaskTypePatterns: string[]; taskStore: TaskStore; @@ -90,19 +89,16 @@ export async function claimAvailableTasksMget( async function claimAvailableTasks(opts: TaskClaimerOpts): Promise { const { getCapacity, claimOwnershipUntil, batches, events$, taskStore, taskPartitioner } = opts; - const { definitions, unusedTypes, excludedTaskTypes, taskMaxAttempts } = opts; + const { definitions, excludedTaskTypes, taskMaxAttempts } = opts; const logger = createWrappedLogger({ logger: opts.logger, tags: [claimAvailableTasksMget.name] }); const initialCapacity = getCapacity(); const stopTaskTimer = startTaskTimer(); - const removedTypes = new Set(unusedTypes); // REMOVED_TYPES - // get a list of candidate tasks to claim, with their version info const { docs, versionMap } = await searchAvailableTasks({ definitions, taskTypes: new Set(definitions.getAllTypes()), excludedTaskTypePatterns: excludedTaskTypes, - removedTypes, taskStore, events$, claimOwnershipUntil, @@ -125,18 +121,12 @@ async function claimAvailableTasks(opts: TaskClaimerOpts): Promise `task:${doc.id}`)); - // filter out stale, missing and removed tasks + // filter out stale and missing tasks const currentTasks: ConcreteTaskInstance[] = []; const staleTasks: ConcreteTaskInstance[] = []; const missingTasks: ConcreteTaskInstance[] = []; - const removedTasks: ConcreteTaskInstance[] = []; for (const searchDoc of docs) { - if (removedTypes.has(searchDoc.taskType)) { - removedTasks.push(searchDoc); - continue; - } - const searchVersion = versionMap.get(searchDoc.id); const latestVersion = docLatestVersions.get(`task:${searchDoc.id}`); if (!searchVersion || !latestVersion) { @@ -236,42 +226,8 @@ async function claimAvailableTasks(opts: TaskClaimerOpts): Promise 0) { - const tasksToRemove = Array.from(removedTasks); - const tasksToRemoveUpdates: PartialConcreteTaskInstance[] = []; - for (const task of tasksToRemove) { - tasksToRemoveUpdates.push({ - id: task.id, - status: TaskStatus.Unrecognized, - }); - } - - // don't worry too much about errors, we'll get them next time - try { - const removeResults = await taskStore.bulkPartialUpdate(tasksToRemoveUpdates); - for (const removeResult of removeResults) { - if (isOk(removeResult)) { - removedCount++; - } else { - const { id, type, error } = removeResult.error; - logger.warn( - `Error updating task ${id}:${type} to mark as unrecognized during claim: ${JSON.stringify( - error - )}` - ); - } - } - } catch (err) { - // swallow the error because this is unrelated to the claim cycle - logger.warn(`Error updating tasks to mark as unrecognized during claim: ${err}`); - } - } - // TODO: need a better way to generate stats - const message = `task claimer claimed: ${fullTasksToRun.length}; stale: ${staleTasks.length}; conflicts: ${conflicts}; missing: ${missingTasks.length}; capacity reached: ${leftOverTasks.length}; updateErrors: ${bulkUpdateErrors}; getErrors: ${bulkGetErrors}; removed: ${removedCount};`; + const message = `task claimer claimed: ${fullTasksToRun.length}; stale: ${staleTasks.length}; conflicts: ${conflicts}; missing: ${missingTasks.length}; capacity reached: ${leftOverTasks.length}; updateErrors: ${bulkUpdateErrors}; getErrors: ${bulkGetErrors};`; logger.debug(message); // build results @@ -306,7 +262,6 @@ export const NO_ASSIGNED_PARTITIONS_WARNING_INTERVAL = 60000; async function searchAvailableTasks({ definitions, taskTypes, - removedTypes, excludedTaskTypePatterns, taskStore, getCapacity, @@ -318,7 +273,6 @@ async function searchAvailableTasks({ const claimPartitions = buildClaimPartitions({ types: taskTypes, excludedTaskTypes, - removedTypes, getCapacity, definitions, }); @@ -352,10 +306,7 @@ async function searchAvailableTasks({ // Task must be enabled EnabledTask, // a task type that's not excluded (may be removed or not) - OneOfTaskTypes( - 'task.taskType', - claimPartitions.unlimitedTypes.concat(Array.from(removedTypes)) - ), + OneOfTaskTypes('task.taskType', claimPartitions.unlimitedTypes), // Either a task with idle status and runAt <= now or // status running or claiming with a retryAt <= now. shouldBeOneOf(IdleTaskWithExpiredRunAt, RunningOrClaimingTaskWithExpiredRetryAt), @@ -407,7 +358,6 @@ async function searchAvailableTasks({ } interface ClaimPartitions { - removedTypes: string[]; unlimitedTypes: string[]; limitedTypes: Map; } @@ -415,30 +365,23 @@ interface ClaimPartitions { interface BuildClaimPartitionsOpts { types: Set; excludedTaskTypes: Set; - removedTypes: Set; getCapacity: (taskType?: string) => number; definitions: TaskTypeDictionary; } function buildClaimPartitions(opts: BuildClaimPartitionsOpts): ClaimPartitions { const result: ClaimPartitions = { - removedTypes: [], unlimitedTypes: [], limitedTypes: new Map(), }; - const { types, excludedTaskTypes, removedTypes, getCapacity, definitions } = opts; + const { types, excludedTaskTypes, getCapacity, definitions } = opts; for (const type of types) { const definition = definitions.get(type); if (definition == null) continue; if (excludedTaskTypes.has(type)) continue; - if (removedTypes.has(type)) { - result.removedTypes.push(type); - continue; - } - if (definition.maxConcurrency == null) { result.unlimitedTypes.push(definition.type); continue; diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts index 13e6faf2de0fd..623693e71c54d 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.test.ts @@ -99,14 +99,12 @@ describe('TaskClaiming', () => { hits = [generateFakeTasks(1)], versionConflicts = 2, excludedTaskTypes = [], - unusedTaskTypes = [], }: { storeOpts: Partial; taskClaimingOpts: Partial; hits?: ConcreteTaskInstance[][]; versionConflicts?: number; excludedTaskTypes?: string[]; - unusedTaskTypes?: string[]; }) { const definitions = storeOpts.definitions ?? taskDefinitions; const store = taskStoreMock.create({ taskManagerId: storeOpts.taskManagerId }); @@ -136,7 +134,6 @@ describe('TaskClaiming', () => { definitions, taskStore: store, excludedTaskTypes, - unusedTypes: unusedTaskTypes, maxAttempts: taskClaimingOpts.maxAttempts ?? 2, getAvailableCapacity: taskClaimingOpts.getAvailableCapacity ?? (() => 10), taskPartitioner, @@ -153,7 +150,6 @@ describe('TaskClaiming', () => { hits = [generateFakeTasks(1)], versionConflicts = 2, excludedTaskTypes = [], - unusedTaskTypes = [], }: { storeOpts: Partial; taskClaimingOpts: Partial; @@ -161,14 +157,12 @@ describe('TaskClaiming', () => { hits?: ConcreteTaskInstance[][]; versionConflicts?: number; excludedTaskTypes?: string[]; - unusedTaskTypes?: string[]; }) { const getCapacity = taskClaimingOpts.getAvailableCapacity ?? (() => 10); const { taskClaiming, store } = initialiseTestClaiming({ storeOpts, taskClaimingOpts, excludedTaskTypes, - unusedTaskTypes, hits, versionConflicts, }); @@ -471,7 +465,6 @@ if (doc['task.runAt'].size()!=0) { 'anotherLimitedToOne', 'limitedToTwo', ], - unusedTaskTypes: [], taskMaxAttempts: { unlimited: maxAttempts, }, @@ -493,7 +486,6 @@ if (doc['task.runAt'].size()!=0) { 'anotherLimitedToOne', 'limitedToTwo', ], - unusedTaskTypes: [], taskMaxAttempts: { limitedToOne: maxAttempts, }, @@ -640,7 +632,6 @@ if (doc['task.runAt'].size()!=0) { }, taskPartitioner, excludedTaskTypes: [], - unusedTypes: [], }); const resultOrErr = await taskClaiming.claimAvailableTasksIfCapacityIsAvailable({ @@ -848,129 +839,6 @@ if (doc['task.runAt'].size()!=0) { expect(firstCycle).not.toMatchObject(secondCycle); }); - test('it passes any unusedTaskTypes to script', async () => { - const maxAttempts = _.random(2, 43); - const customMaxAttempts = _.random(44, 100); - const taskManagerId = uuidv1(); - const fieldUpdates = { - ownerId: taskManagerId, - retryAt: new Date(Date.now()), - }; - const definitions = new TaskTypeDictionary(mockLogger()); - definitions.registerTaskDefinitions({ - foo: { - title: 'foo', - createTaskRunner: jest.fn(), - }, - bar: { - title: 'bar', - maxAttempts: customMaxAttempts, - createTaskRunner: jest.fn(), - }, - foobar: { - title: 'foobar', - maxAttempts: customMaxAttempts, - createTaskRunner: jest.fn(), - }, - }); - - const { - args: { - updateByQuery: [{ query, script }], - }, - } = await testClaimAvailableTasks({ - storeOpts: { - definitions, - taskManagerId, - }, - taskClaimingOpts: { - maxAttempts, - }, - claimingOpts: { - claimOwnershipUntil: new Date(), - }, - excludedTaskTypes: ['foobar'], - unusedTaskTypes: ['barfoo'], - }); - expect(query).toMatchObject({ - bool: { - must: [ - { - bool: { - must: [ - { - term: { - 'task.enabled': true, - }, - }, - ], - }, - }, - { - bool: { - should: [ - { - bool: { - must: [ - { term: { 'task.status': 'idle' } }, - { range: { 'task.runAt': { lte: 'now' } } }, - ], - }, - }, - { - bool: { - must: [ - { - bool: { - should: [ - { term: { 'task.status': 'running' } }, - { term: { 'task.status': 'claiming' } }, - ], - }, - }, - { range: { 'task.retryAt': { lte: 'now' } } }, - ], - }, - }, - ], - }, - }, - ], - filter: [ - { - bool: { - must_not: [ - { - bool: { - should: [ - { term: { 'task.status': 'running' } }, - { term: { 'task.status': 'claiming' } }, - ], - must: { range: { 'task.retryAt': { gt: 'now' } } }, - }, - }, - ], - }, - }, - ], - }, - }); - expect(script).toMatchObject({ - source: expect.any(String), - lang: 'painless', - params: { - fieldUpdates, - claimableTaskTypes: ['foo', 'bar'], - skippedTaskTypes: ['foobar'], - unusedTaskTypes: ['barfoo'], - taskMaxAttempts: { - bar: customMaxAttempts, - foo: maxAttempts, - }, - }, - }); - }); - test('it claims tasks by setting their ownerId, status and retryAt', async () => { const taskManagerId = uuidv1(); const claimOwnershipUntil = new Date(Date.now()); @@ -1356,7 +1224,6 @@ if (doc['task.runAt'].size()!=0) { strategy: 'update_by_query', definitions, excludedTaskTypes: [], - unusedTypes: [], taskStore, maxAttempts: 2, getAvailableCapacity, diff --git a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts index 5a4bccb43b984..fdfd09e07f9c7 100644 --- a/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts +++ b/x-pack/plugins/task_manager/server/task_claimers/strategy_update_by_query.ts @@ -51,7 +51,6 @@ interface OwnershipClaimingOpts { taskStore: TaskStore; events$: Subject; definitions: TaskTypeDictionary; - unusedTypes: string[]; excludedTaskTypes: string[]; taskMaxAttempts: Record; } @@ -60,7 +59,7 @@ export async function claimAvailableTasksUpdateByQuery( opts: TaskClaimerOpts ): Promise { const { getCapacity, claimOwnershipUntil, batches, events$, taskStore } = opts; - const { definitions, unusedTypes, excludedTaskTypes, taskMaxAttempts } = opts; + const { definitions, excludedTaskTypes, taskMaxAttempts } = opts; const initialCapacity = getCapacity(); let accumulatedResult = getEmptyClaimOwnershipResult(); @@ -83,7 +82,6 @@ export async function claimAvailableTasksUpdateByQuery( taskTypes: isLimited(batch) ? new Set([batch.tasksTypes]) : batch.tasksTypes, taskStore, definitions, - unusedTypes, excludedTaskTypes, taskMaxAttempts, }); @@ -137,7 +135,6 @@ async function markAvailableTasksAsClaimed({ claimOwnershipUntil, size, taskTypes, - unusedTypes, taskMaxAttempts, }: OwnershipClaimingOpts): Promise { const { taskTypesToSkip = [], taskTypesToClaim = [] } = groupBy( @@ -164,7 +161,6 @@ async function markAvailableTasksAsClaimed({ }, claimableTaskTypes: taskTypesToClaim, skippedTaskTypes: taskTypesToSkip, - unusedTaskTypes: unusedTypes, taskMaxAttempts: pick(taskMaxAttempts, taskTypesToClaim), }); diff --git a/x-pack/plugins/task_manager/tsconfig.json b/x-pack/plugins/task_manager/tsconfig.json index 55ad764c5bdcd..b11eaaf44a905 100644 --- a/x-pack/plugins/task_manager/tsconfig.json +++ b/x-pack/plugins/task_manager/tsconfig.json @@ -27,7 +27,8 @@ "@kbn/logging", "@kbn/core-lifecycle-server", "@kbn/cloud-plugin", - "@kbn/core-saved-objects-base-server-internal" + "@kbn/core-saved-objects-base-server-internal", + "@kbn/core-elasticsearch-server", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9d89589574213..f03c509001511 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -862,11 +862,7 @@ "controls.controlGroup.manageControl.dataSource.dataViewTitle": "Vue de données", "controls.controlGroup.manageControl.dataSource.fieldListErrorTitle": "Erreur lors du chargement de la liste des champs", "controls.controlGroup.manageControl.dataSource.fieldTitle": "Champ", - "controls.controlGroup.manageControl.dataSource.formGroupDescription": "Sélectionnez la vue de données et le champ pour lesquels vous voulez créer un contrôle.", - "controls.controlGroup.manageControl.dataSource.formGroupTitle": "Source de données", "controls.controlGroup.manageControl.dataSource.selectDataViewMessage": "Veuillez sélectionner une vue de données", - "controls.controlGroup.manageControl.displaySettings.formGroupDescription": "Changez la manière dont le contrôle apparaît sur votre tableau de bord.", - "controls.controlGroup.manageControl.displaySettings.formGroupTitle": "Paramètres d'affichage", "controls.controlGroup.manageControl.displaySettings.growSwitchTitle": "Augmenter la largeur en fonction de l'espace disponible", "controls.controlGroup.manageControl.displaySettings.titleInputTitle": "Étiquette", "controls.controlGroup.manageControl.displaySettings.widthInputTitle": "Largeur minimale", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c923e7c19a858..0b5587b2bd916 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -864,11 +864,7 @@ "controls.controlGroup.manageControl.dataSource.dataViewTitle": "データビュー", "controls.controlGroup.manageControl.dataSource.fieldListErrorTitle": "フィールドリストの読み込みエラー", "controls.controlGroup.manageControl.dataSource.fieldTitle": "フィールド", - "controls.controlGroup.manageControl.dataSource.formGroupDescription": "コントロールを作成するデータビューとフィールドを選択します。", - "controls.controlGroup.manageControl.dataSource.formGroupTitle": "データソース", "controls.controlGroup.manageControl.dataSource.selectDataViewMessage": "データビューを選択してください", - "controls.controlGroup.manageControl.displaySettings.formGroupDescription": "ダッシュボードにコントロールを表示する方法を変更します。", - "controls.controlGroup.manageControl.displaySettings.formGroupTitle": "表示設定", "controls.controlGroup.manageControl.displaySettings.growSwitchTitle": "空きスペースに合わせて幅を拡大", "controls.controlGroup.manageControl.displaySettings.titleInputTitle": "ラベル", "controls.controlGroup.manageControl.displaySettings.widthInputTitle": "最小幅", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ad65daedc7406..4d4db174396e2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -856,11 +856,7 @@ "controls.controlGroup.manageControl.dataSource.dataViewTitle": "数据视图", "controls.controlGroup.manageControl.dataSource.fieldListErrorTitle": "加载字段列表时出错", "controls.controlGroup.manageControl.dataSource.fieldTitle": "字段", - "controls.controlGroup.manageControl.dataSource.formGroupDescription": "选择要为其创建控件的数据视图和字段。", - "controls.controlGroup.manageControl.dataSource.formGroupTitle": "数据源", "controls.controlGroup.manageControl.dataSource.selectDataViewMessage": "请选择数据视图", - "controls.controlGroup.manageControl.displaySettings.formGroupDescription": "更改控件在仪表板上的显示方式。", - "controls.controlGroup.manageControl.displaySettings.formGroupTitle": "显示设置", "controls.controlGroup.manageControl.displaySettings.growSwitchTitle": "扩大宽度以适应可用空间", "controls.controlGroup.manageControl.displaySettings.titleInputTitle": "标签", "controls.controlGroup.manageControl.displaySettings.widthInputTitle": "最小宽度", diff --git a/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts b/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts index 6e4eba065ec70..f6e3383270436 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/task_manager_fixture/server/plugin.ts @@ -95,6 +95,34 @@ export class SampleTaskManagerFixturePlugin return res.ok({ body: {} }); } ); + + router.post( + { + path: '/api/alerting_tasks/run_mark_tasks_as_unrecognized', + validate: { + body: schema.object({}), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + try { + const taskManager = await this.taskManagerStart; + await taskManager.ensureScheduled({ + id: 'mark_removed_tasks_as_unrecognized', + taskType: 'task_manager:mark_removed_tasks_as_unrecognized', + schedule: { interval: '1h' }, + state: {}, + params: {}, + }); + return res.ok({ body: await taskManager.runSoon('mark_removed_tasks_as_unrecognized') }); + } catch (err) { + return res.ok({ body: { id: 'mark_removed_tasks_as_unrecognized', error: `${err}` } }); + } + } + ); } public start(core: CoreStart, { taskManager }: SampleTaskManagerFixtureStartDeps) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts index 0086dd2679c74..f05075be810a1 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/scheduled_task_id.ts @@ -138,6 +138,12 @@ export default function createScheduledTaskIdTests({ getService }: FtrProviderCo // When we enable the rule, the unrecognized task should be removed and a new // task created in its place + await supertestWithoutAuth + .post('/api/alerting_tasks/run_mark_tasks_as_unrecognized') + .set('kbn-xsrf', 'foo') + .send({}) + .expect(200); + // scheduled task should exist and be unrecognized await retry.try(async () => { const taskRecordLoaded = await getScheduledTask(RULE_ID); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts new file mode 100644 index 0000000000000..6afc2e9eca63b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/constants/archiver.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +type ArchiveName = + | '8.0.0' + | 'apm_8.0.0' + | 'apm_mappings_only_8.0.0' + | 'infra_metrics_and_apm' + | 'metrics_8.0.0' + | 'ml_8.0.0' + | 'observability_overview' + | 'rum_8.0.0' + | 'rum_test_data'; + +export const ARCHIVER_ROUTES: { [key in ArchiveName]: string } = { + '8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0', + 'apm_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0', + 'apm_mappings_only_8.0.0': + 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0', + infra_metrics_and_apm: + 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/infra_metrics_and_apm', + 'metrics_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/metrics_8.0.0', + 'ml_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/ml_8.0.0', + observability_overview: + 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview', + 'rum_8.0.0': 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0', + rum_test_data: 'x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_test_data', +}; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/failed_transactions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/failed_transactions.spec.ts new file mode 100644 index 0000000000000..549f48009197f --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/failed_transactions.spec.ts @@ -0,0 +1,236 @@ +/* + * 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 { orderBy } from 'lodash'; +import expect from '@kbn/expect'; +import type { FailedTransactionsCorrelationsResponse } from '@kbn/apm-plugin/common/correlations/failed_transactions_correlations/types'; +import { EVENT_OUTCOME } from '@kbn/apm-plugin/common/es_fields/apm'; +import { EventOutcome } from '@kbn/apm-plugin/common/event_outcome'; +import { LatencyDistributionChartType } from '@kbn/apm-plugin/common/latency_distribution_chart_types'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; + +// These tests go through the full sequence of queries required +// to get the final results for a failed transactions correlation analysis. +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); + // This matches the parameters used for the other tab's queries in `../correlations/*`. + const getOptions = () => ({ + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }); + + describe('failed transactions', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(overallDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${overallDistributionResponse.status}'` + ); + + const errorDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(errorDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${errorDistributionResponse.status}'` + ); + + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', + params: { + query: getOptions(), + }, + }); + + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` + ); + + const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/p_values/transactions', + params: { + body: { + ...getOptions(), + fieldCandidates: fieldCandidatesResponse.body?.fieldCandidates, + }, + }, + }); + + expect(failedTransactionsCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` + ); + + const finalRawResponse: FailedTransactionsCorrelationsResponse = { + ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + failedTransactionsCorrelations: + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, + }; + + expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( + 0, + `Expected 0 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` + ); + }); + }); + + describe('with data', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + + it('runs queries and returns results', async () => { + const overallDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(overallDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${overallDistributionResponse.status}'` + ); + + const errorDistributionResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', + params: { + body: { + ...getOptions(), + percentileThreshold: 95, + termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], + chartType: LatencyDistributionChartType.failedTransactionsCorrelations, + }, + }, + }); + + expect(errorDistributionResponse.status).to.eql( + 200, + `Expected status to be '200', got '${errorDistributionResponse.status}'` + ); + + const fieldCandidatesResponse = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', + params: { + query: getOptions(), + }, + }); + + expect(fieldCandidatesResponse.status).to.eql( + 200, + `Expected status to be '200', got '${fieldCandidatesResponse.status}'` + ); + + const fieldCandidates = fieldCandidatesResponse.body?.fieldCandidates.filter( + (t) => !(t === EVENT_OUTCOME) + ); + + // Identified 80 fieldCandidates. + expect(fieldCandidates.length).to.eql( + 80, + `Expected field candidates length to be '80', got '${fieldCandidates.length}'` + ); + + const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ + endpoint: 'POST /internal/apm/correlations/p_values/transactions', + params: { + body: { + ...getOptions(), + fieldCandidates, + }, + }, + }); + + expect(failedTransactionsCorrelationsResponse.status).to.eql( + 200, + `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` + ); + + const fieldsToSample = new Set(); + if ( + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.length > 0 + ) { + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.forEach( + (d) => { + fieldsToSample.add(d.fieldName); + } + ); + } + + const finalRawResponse: FailedTransactionsCorrelationsResponse = { + ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, + percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, + overallHistogram: overallDistributionResponse.body?.overallHistogram, + errorHistogram: errorDistributionResponse.body?.overallHistogram, + failedTransactionsCorrelations: + failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, + }; + + expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); + expect(finalRawResponse?.errorHistogram?.length).to.be(101); + expect(finalRawResponse?.overallHistogram?.length).to.be(101); + + expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( + 29, + `Expected 29 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` + ); + + const sortedCorrelations = orderBy( + finalRawResponse?.failedTransactionsCorrelations, + ['score', 'fieldName', 'fieldValue'], + ['desc', 'asc', 'asc'] + ); + const correlation = sortedCorrelations?.[0]; + + expect(typeof correlation).to.be('object'); + expect(correlation?.doc_count).to.be(31); + expect(correlation?.score).to.be(83.70467673605746); + expect(correlation?.bg_count).to.be(31); + expect(correlation?.fieldName).to.be('transaction.result'); + expect(correlation?.fieldValue).to.be('HTTP 5xx'); + expect(typeof correlation?.pValue).to.be('number'); + expect(typeof correlation?.normalizedScore).to.be('number'); + expect(typeof correlation?.failurePercentage).to.be('number'); + expect(typeof correlation?.successPercentage).to.be('number'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_candidates.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_candidates.spec.ts new file mode 100644 index 0000000000000..8db9a7df05fd3 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_candidates.spec.ts @@ -0,0 +1,63 @@ +/* + * 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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); + + const endpoint = 'GET /internal/apm/correlations/field_candidates/transactions'; + + const getOptions = () => ({ + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + }, + }, + }); + + describe('field candidates', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + // If the source indices are empty, there will be no field candidates + // because of the `include_empty_fields: false` option in the query. + expect(response.body?.fieldCandidates.length).to.be(0); + }); + }); + + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + + it('returns field candidates', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + expect(response.body?.fieldCandidates.length).to.be(81); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_value_pairs.spec.ts similarity index 59% rename from x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_value_pairs.spec.ts index 4765e83342e52..9fcd438421b6a 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/field_value_pairs.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/field_value_pairs.spec.ts @@ -6,11 +6,12 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); const endpoint = 'POST /internal/apm/correlations/field_value_pairs/transactions'; @@ -41,22 +42,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when('field value pairs without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); + describe('field value pairs', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); - expect(response.status).to.be(200); - expect(response.body?.fieldValuePairs.length).to.be(0); + expect(response.status).to.be(200); + expect(response.body?.fieldValuePairs.length).to.be(0); + }); }); - }); - registry.when( - 'field value pairs with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + it('returns field value pairs', async () => { const response = await apmApiClient.readUser({ endpoint, @@ -66,6 +72,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body?.fieldValuePairs.length).to.be(124); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts new file mode 100644 index 0000000000000..660556edb8d79 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('correlations', () => { + loadTestFile(require.resolve('./failed_transactions.spec.ts')); + loadTestFile(require.resolve('./field_candidates.spec.ts')); + loadTestFile(require.resolve('./field_value_pairs.spec.ts')); + loadTestFile(require.resolve('./latency.spec.ts')); + loadTestFile(require.resolve('./p_values.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/latency.spec.ts similarity index 93% rename from x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/latency.spec.ts index 5326136976428..e0080806f6a0e 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/latency.spec.ts @@ -14,13 +14,14 @@ import type { LatencyCorrelationsResponse, } from '@kbn/apm-plugin/common/correlations/latency_correlations/types'; import { LatencyDistributionChartType } from '@kbn/apm-plugin/common/latency_distribution_chart_types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; // These tests go through the full sequence of queries required // to get the final results for a latency correlation analysis. -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); // This matches the parameters used for the other tab's queries in `../correlations/*`. const getOptions = () => ({ @@ -30,10 +31,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { kuery: '', }); - registry.when( - 'correlations latency overall without data', - { config: 'trial', archives: [] }, - () => { + describe('latency', () => { + describe('overall without data', () => { it('handles the empty state', async () => { const overallDistributionResponse = await apmApiClient.readUser({ endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', @@ -104,13 +103,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(finalRawResponse?.overallHistogram).to.be(undefined); expect(finalRawResponse?.latencyCorrelations?.length).to.be(0); }); - } - ); + }); + + describe('with data and opbeans-node args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); - registry.when( - 'correlations latency with data and opbeans-node args', - { config: 'trial', archives: ['8.0.0'] }, - () => { // putting this into a single `it` because the responses depend on each other it('runs queries and returns results', async () => { const overallDistributionResponse = await apmApiClient.readUser({ @@ -250,6 +252,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(correlation?.ksTest).to.be(1.9848961005439386e-12); expect(correlation?.histogram?.length).to.be(101); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/p_values.spec.ts similarity index 58% rename from x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/p_values.spec.ts index 42a9fdadbb480..ba6e3384cec93 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/p_values.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/p_values.spec.ts @@ -6,11 +6,12 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); const endpoint = 'POST /internal/apm/correlations/p_values/transactions'; @@ -41,22 +42,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when('p values without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); + describe('p values', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); - expect(response.status).to.be(200); - expect(response.body?.failedTransactionsCorrelations.length).to.be(0); + expect(response.status).to.be(200); + expect(response.body?.failedTransactionsCorrelations.length).to.be(0); + }); }); - }); - registry.when( - 'p values with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + it('returns p values', async () => { const response = await apmApiClient.readUser({ endpoint, @@ -66,6 +72,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body?.failedTransactionsCorrelations.length).to.be(15); }); - } - ); + }); + }); } diff --git a/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/significant_correlations.spec.ts similarity index 71% rename from x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/significant_correlations.spec.ts index d4450c192a029..e1f968d178868 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/significant_correlations.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/correlations/significant_correlations.spec.ts @@ -6,11 +6,12 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { ARCHIVER_ROUTES } from '../constants/archiver'; -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const esArchiver = getService('esArchiver'); const endpoint = 'POST /internal/apm/correlations/significant_correlations/transactions'; @@ -65,22 +66,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - registry.when('significant correlations without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); + describe('significant correlations', () => { + describe('without data', () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); - expect(response.status).to.be(200); - expect(response.body?.latencyCorrelations.length).to.be(0); + expect(response.status).to.be(200); + expect(response.body?.latencyCorrelations.length).to.be(0); + }); }); - }); - registry.when( - 'significant correlations with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { + describe('with data and default args', () => { + before(async () => { + await esArchiver.load(ARCHIVER_ROUTES['8.0.0']); + }); + after(async () => { + await esArchiver.unload(ARCHIVER_ROUTES['8.0.0']); + }); + it('returns significant correlations', async () => { const response = await apmApiClient.readUser({ endpoint, @@ -90,6 +96,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body?.latencyCorrelations.length).to.be(7); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index f8c0352984473..f1e8fc381a072 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -15,6 +15,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./mobile')); loadTestFile(require.resolve('./custom_dashboards')); loadTestFile(require.resolve('./dependencies')); + loadTestFile(require.resolve('./correlations')); loadTestFile(require.resolve('./entities')); loadTestFile(require.resolve('./cold_start')); }); diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json index a64e037343bb3..c79a4c6b52309 100644 --- a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json @@ -3786,10 +3786,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-error" - }, "mapping": { "total_fields": { "limit": "2000" @@ -4243,8 +4239,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -8183,10 +8178,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-metric" - }, "mapping": { "total_fields": { "limit": "2000" @@ -8640,8 +8631,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -12871,8 +12861,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -16653,10 +16642,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-profile" - }, "mapping": { "total_fields": { "limit": "2000" @@ -17110,8 +17095,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -20899,10 +20883,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-span" - }, "mapping": { "total_fields": { "limit": "2000" @@ -21356,8 +21336,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } @@ -25242,10 +25221,6 @@ "index": { "auto_expand_replicas": "0-1", "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-transaction" - }, "mapping": { "total_fields": { "limit": "2000" @@ -25699,8 +25674,7 @@ "transaction.message.queue.name", "fields.*" ] - }, - "refresh_interval": "1ms" + } } } } diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts deleted file mode 100644 index 13754f6c7eb5a..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.spec.ts +++ /dev/null @@ -1,223 +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 { orderBy } from 'lodash'; -import expect from '@kbn/expect'; -import type { FailedTransactionsCorrelationsResponse } from '@kbn/apm-plugin/common/correlations/failed_transactions_correlations/types'; -import { EVENT_OUTCOME } from '@kbn/apm-plugin/common/es_fields/apm'; -import { EventOutcome } from '@kbn/apm-plugin/common/event_outcome'; -import { LatencyDistributionChartType } from '@kbn/apm-plugin/common/latency_distribution_chart_types'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -// These tests go through the full sequence of queries required -// to get the final results for a failed transactions correlation analysis. -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - - // This matches the parameters used for the other tab's queries in `../correlations/*`. - const getOptions = () => ({ - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - }); - - registry.when('failed transactions without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(overallDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${overallDistributionResponse.status}'` - ); - - const errorDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(errorDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${errorDistributionResponse.status}'` - ); - - const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', - params: { - query: getOptions(), - }, - }); - - expect(fieldCandidatesResponse.status).to.eql( - 200, - `Expected status to be '200', got '${fieldCandidatesResponse.status}'` - ); - - const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/p_values/transactions', - params: { - body: { - ...getOptions(), - fieldCandidates: fieldCandidatesResponse.body?.fieldCandidates, - }, - }, - }); - - expect(failedTransactionsCorrelationsResponse.status).to.eql( - 200, - `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` - ); - - const finalRawResponse: FailedTransactionsCorrelationsResponse = { - ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, - percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, - overallHistogram: overallDistributionResponse.body?.overallHistogram, - failedTransactionsCorrelations: - failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, - }; - - expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( - 0, - `Expected 0 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` - ); - }); - }); - - registry.when('failed transactions with data', { config: 'trial', archives: ['8.0.0'] }, () => { - it('runs queries and returns results', async () => { - const overallDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(overallDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${overallDistributionResponse.status}'` - ); - - const errorDistributionResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/latency/overall_distribution/transactions', - params: { - body: { - ...getOptions(), - percentileThreshold: 95, - termFilters: [{ fieldName: EVENT_OUTCOME, fieldValue: EventOutcome.failure }], - chartType: LatencyDistributionChartType.failedTransactionsCorrelations, - }, - }, - }); - - expect(errorDistributionResponse.status).to.eql( - 200, - `Expected status to be '200', got '${errorDistributionResponse.status}'` - ); - - const fieldCandidatesResponse = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/correlations/field_candidates/transactions', - params: { - query: getOptions(), - }, - }); - - expect(fieldCandidatesResponse.status).to.eql( - 200, - `Expected status to be '200', got '${fieldCandidatesResponse.status}'` - ); - - const fieldCandidates = fieldCandidatesResponse.body?.fieldCandidates.filter( - (t) => !(t === EVENT_OUTCOME) - ); - - // Identified 80 fieldCandidates. - expect(fieldCandidates.length).to.eql( - 80, - `Expected field candidates length to be '80', got '${fieldCandidates.length}'` - ); - - const failedTransactionsCorrelationsResponse = await apmApiClient.readUser({ - endpoint: 'POST /internal/apm/correlations/p_values/transactions', - params: { - body: { - ...getOptions(), - fieldCandidates, - }, - }, - }); - - expect(failedTransactionsCorrelationsResponse.status).to.eql( - 200, - `Expected status to be '200', got '${failedTransactionsCorrelationsResponse.status}'` - ); - - const fieldsToSample = new Set(); - if (failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.length > 0) { - failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations.forEach((d) => { - fieldsToSample.add(d.fieldName); - }); - } - - const finalRawResponse: FailedTransactionsCorrelationsResponse = { - ccsWarning: failedTransactionsCorrelationsResponse.body?.ccsWarning, - percentileThresholdValue: overallDistributionResponse.body?.percentileThresholdValue, - overallHistogram: overallDistributionResponse.body?.overallHistogram, - errorHistogram: errorDistributionResponse.body?.overallHistogram, - failedTransactionsCorrelations: - failedTransactionsCorrelationsResponse.body?.failedTransactionsCorrelations, - }; - - expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); - expect(finalRawResponse?.errorHistogram?.length).to.be(101); - expect(finalRawResponse?.overallHistogram?.length).to.be(101); - - expect(finalRawResponse?.failedTransactionsCorrelations?.length).to.eql( - 29, - `Expected 29 identified correlations, got ${finalRawResponse?.failedTransactionsCorrelations?.length}.` - ); - - const sortedCorrelations = orderBy( - finalRawResponse?.failedTransactionsCorrelations, - ['score', 'fieldName', 'fieldValue'], - ['desc', 'asc', 'asc'] - ); - const correlation = sortedCorrelations?.[0]; - - expect(typeof correlation).to.be('object'); - expect(correlation?.doc_count).to.be(31); - expect(correlation?.score).to.be(83.70467673605746); - expect(correlation?.bg_count).to.be(31); - expect(correlation?.fieldName).to.be('transaction.result'); - expect(correlation?.fieldValue).to.be('HTTP 5xx'); - expect(typeof correlation?.pValue).to.be('number'); - expect(typeof correlation?.normalizedScore).to.be('number'); - expect(typeof correlation?.failurePercentage).to.be('number'); - expect(typeof correlation?.successPercentage).to.be('number'); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts b/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts deleted file mode 100644 index 4a5472cf5cb23..0000000000000 --- a/x-pack/test/apm_api_integration/tests/correlations/field_candidates.spec.ts +++ /dev/null @@ -1,57 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const apmApiClient = getService('apmApiClient'); - const registry = getService('registry'); - - const endpoint = 'GET /internal/apm/correlations/field_candidates/transactions'; - - const getOptions = () => ({ - params: { - query: { - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - }, - }, - }); - - registry.when('field candidates without data', { config: 'trial', archives: [] }, () => { - it('handles the empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); - - expect(response.status).to.be(200); - // If the source indices are empty, there will be no field candidates - // because of the `include_empty_fields: false` option in the query. - expect(response.body?.fieldCandidates.length).to.be(0); - }); - }); - - registry.when( - 'field candidates with data and default args', - { config: 'trial', archives: ['8.0.0'] }, - () => { - it('returns field candidates', async () => { - const response = await apmApiClient.readUser({ - endpoint, - ...getOptions(), - }); - - expect(response.status).to.eql(200); - expect(response.body?.fieldCandidates.length).to.be(81); - }); - } - ); -} diff --git a/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json b/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json index 9f536d0bb6dc9..37f7ebdff5fb1 100644 --- a/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json +++ b/x-pack/test/cloud_security_posture_api/es_archives/logs_gcp_audit/data.json @@ -497,3 +497,123 @@ } } } + +{ + "type": "doc", + "value": { + "data_stream": "logs-gcp.audit-default", + "id": "5", + "index": ".ds-logs-gcp.audit-default-2024.10.07-000001", + "source": { + "@timestamp": "2024-09-01T12:34:56.789Z", + "actor": { + "entity": { + "id": "admin5@example.com" + } + }, + "client": { + "user": { + "email": "admin5@example.com" + } + }, + "cloud": { + "project": { + "id": "your-project-id" + }, + "provider": "gcp" + }, + "ecs": { + "version": "8.11.0" + }, + "event": { + "action": "google.iam.admin.v1.ListRoles", + "agent_id_status": "missing", + "category": [ + "session", + "network", + "configuration" + ], + "id": "without target", + "ingested": "2024-10-07T17:47:35Z", + "kind": "event", + "outcome": "success", + "provider": "activity", + "type": [ + "end", + "access", + "allowed" + ] + }, + "gcp": { + "audit": { + "authorization_info": [ + { + "granted": true, + "permission": "iam.roles.create", + "resource": "projects/your-project-id" + } + ], + "logentry_operation": { + "id": "operation-0987654321" + }, + "request": { + "@type": "type.googleapis.com/google.iam.admin.v1.CreateRoleRequest", + "parent": "projects/your-project-id", + "role": { + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "title": "Custom Role" + }, + "roleId": "customRole" + }, + "resource_name": "projects/your-project-id/roles/customRole", + "response": { + "@type": "type.googleapis.com/google.iam.admin.v1.Role", + "description": "A custom role with specific permissions", + "includedPermissions": [ + "resourcemanager.projects.get", + "resourcemanager.projects.list" + ], + "name": "projects/your-project-id/roles/customRole", + "stage": "GA", + "title": "Custom Role" + }, + "type": "type.googleapis.com/google.cloud.audit.AuditLog" + } + }, + "log": { + "level": "NOTICE", + "logger": "projects/your-project-id/logs/cloudaudit.googleapis.com%2Factivity" + }, + "related": { + "ip": [ + "10.0.0.1" + ], + "user": [ + "admin3@example.com" + ] + }, + "service": { + "name": "iam.googleapis.com" + }, + "source": { + "ip": "10.0.0.1" + }, + "tags": [ + "_geoip_database_unavailable_GeoLite2-City.mmdb", + "_geoip_database_unavailable_GeoLite2-ASN.mmdb" + ], + "user_agent": { + "device": { + "name": "Other" + }, + "name": "Other", + "original": "google-cloud-sdk/324.0.0" + } + } + } +} diff --git a/x-pack/test/cloud_security_posture_api/routes/graph.ts b/x-pack/test/cloud_security_posture_api/routes/graph.ts index bd2f71ef3b9b2..8043e6e22feb6 100644 --- a/x-pack/test/cloud_security_posture_api/routes/graph.ts +++ b/x-pack/test/cloud_security_posture_api/routes/graph.ts @@ -11,6 +11,8 @@ import { } from '@kbn/core-http-common'; import expect from '@kbn/expect'; import type { Agent } from 'supertest'; +import { ApiMessageCode } from '@kbn/cloud-security-posture-common/types/graph/latest'; +import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/latest'; import { FtrProviderContext } from '../ftr_provider_context'; import { result } from '../utils'; import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; @@ -19,12 +21,13 @@ import { CspSecurityCommonProvider } from './helper/user_roles_utilites'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; + const logger = getService('log'); const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const cspSecurity = CspSecurityCommonProvider(providerContext); - const postGraph = (agent: Agent, body: any, auth?: { user: string; pass: string }) => { + const postGraph = (agent: Agent, body: GraphRequest, auth?: { user: string; pass: string }) => { const req = agent .post('/internal/cloud_security_posture/graph') .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -45,7 +48,6 @@ export default function (providerContext: FtrProviderContext) { supertestWithoutAuth, { query: { - actorIds: [], eventIds: [], start: 'now-1d/d', end: 'now/d', @@ -55,19 +57,7 @@ export default function (providerContext: FtrProviderContext) { user: 'role_security_no_read_user', pass: cspSecurity.getPasswordForUser('role_security_no_read_user'), } - ).expect(result(403)); - }); - }); - - describe('Validation', () => { - it('should return 400 when missing `actorIds` field', async () => { - await postGraph(supertest, { - query: { - eventIds: [], - start: 'now-1d/d', - end: 'now/d', - }, - }).expect(result(400)); + ).expect(result(403, logger)); }); }); @@ -84,10 +74,54 @@ export default function (providerContext: FtrProviderContext) { ); }); - it('should return an empty graph', async () => { + describe('Validation', () => { + it('should return 400 when missing `eventIds` field', async () => { + await postGraph(supertest, { + // @ts-expect-error ignore error for testing + query: { + start: 'now-1d/d', + end: 'now/d', + }, + }).expect(result(400, logger)); + }); + + it('should return 400 when missing `esQuery` field is not of type bool', async () => { + await postGraph(supertest, { + query: { + eventIds: [], + start: 'now-1d/d', + end: 'now/d', + esQuery: { + // @ts-expect-error ignore error for testing + match_all: {}, + }, + }, + }).expect(result(400, logger)); + }); + + it('should return 400 with unsupported `esQuery`', async () => { + await postGraph(supertest, { + query: { + eventIds: [], + start: 'now-1d/d', + end: 'now/d', + esQuery: { + bool: { + filter: [ + { + unsupported: 'unsupported', + }, + ], + }, + }, + }, + }).expect(result(400, logger)); + }); + }); + + it('should return an empty graph / should return 200 when missing `esQuery` field', async () => { const response = await postGraph(supertest, { query: { - actorIds: [], eventIds: [], start: 'now-1d/d', end: 'now/d', @@ -96,20 +130,32 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(0); expect(response.body).to.have.property('edges').length(0); + expect(response.body).not.to.have.property('messages'); }); it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertest, { query: { - actorIds: ['admin@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin@example.com', + }, + }, + ], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -131,7 +177,6 @@ export default function (providerContext: FtrProviderContext) { it('should return a graph with nodes and edges by alert', async () => { const response = await postGraph(supertest, { query: { - actorIds: [], eventIds: ['kabcd1234efgh5678'], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', @@ -140,6 +185,7 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -161,7 +207,6 @@ export default function (providerContext: FtrProviderContext) { it('color of alert of failed event should be danger', async () => { const response = await postGraph(supertest, { query: { - actorIds: [], eventIds: ['failed-event'], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', @@ -170,6 +215,7 @@ export default function (providerContext: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -191,15 +237,26 @@ export default function (providerContext: FtrProviderContext) { it('color of event of failed event should be warning', async () => { const response = await postGraph(supertest, { query: { - actorIds: ['admin2@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin2@example.com', + }, + }, + ], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); @@ -219,18 +276,29 @@ export default function (providerContext: FtrProviderContext) { }); }); - it('2 grouped of events, 1 failed, 1 success', async () => { + it('2 grouped events, 1 failed, 1 success', async () => { const response = await postGraph(supertest, { query: { - actorIds: ['admin3@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin3@example.com', + }, + }, + ], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(5); expect(response.body).to.have.property('edges').length(6); + expect(response.body).not.to.have.property('messages'); expect(response.body.nodes[0].shape).equal('group', 'Groups should be the first nodes'); @@ -247,11 +315,167 @@ export default function (providerContext: FtrProviderContext) { response.body.edges.forEach((edge: any) => { expect(edge).to.have.property('color'); expect(edge.color).equal( - edge.id.includes('outcome(failed)') ? 'warning' : 'primary', + edge.id.includes('outcome(failed)') || + (edge.id.includes('grp(') && !edge.id.includes('outcome(success)')) + ? 'warning' + : 'primary', + `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` + ); + }); + }); + + it('should support more than 1 eventIds', async () => { + const response = await postGraph(supertest, { + query: { + eventIds: ['kabcd1234efgh5678', 'failed-event'], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(5); + expect(response.body).to.have.property('edges').length(4); + expect(response.body).not.to.have.property('messages'); + + response.body.nodes.forEach((node: any) => { + expect(node).to.have.property('color'); + expect(node.color).equal( + 'danger', + `node color mismatched [node: ${node.id}] [actual: ${node.color}]` + ); + }); + + response.body.edges.forEach((edge: any) => { + expect(edge).to.have.property('color'); + expect(edge.color).equal( + 'danger', + `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` + ); + }); + }); + + it('should return a graph with nodes and edges by alert and actor', async () => { + const response = await postGraph(supertest, { + query: { + eventIds: ['kabcd1234efgh5678'], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin2@example.com', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(5); + expect(response.body).to.have.property('edges').length(4); + expect(response.body).not.to.have.property('messages'); + + response.body.nodes.forEach((node: any, idx: number) => { + expect(node).to.have.property('color'); + expect(node.color).equal( + idx <= 2 // First 3 nodes are expected to be colored as danger (ORDER MATTERS, alerts are expected to be first) + ? 'danger' + : node.shape === 'label' && node.id.includes('outcome(failed)') + ? 'warning' + : 'primary', + `node color mismatched [node: ${node.id}] [actual: ${node.color}]` + ); + }); + + response.body.edges.forEach((edge: any, idx: number) => { + expect(edge).to.have.property('color'); + expect(edge.color).equal( + idx <= 1 ? 'danger' : 'warning', `edge color mismatched [edge: ${edge.id}] [actual: ${edge.color}]` ); }); }); + + it('Should filter unknown targets', async () => { + const response = await postGraph(supertest, { + query: { + eventIds: [], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin5@example.com', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(0); + expect(response.body).to.have.property('edges').length(0); + expect(response.body).not.to.have.property('messages'); + }); + + it('Should return unknown targets', async () => { + const response = await postGraph(supertest, { + showUnknownTarget: true, + query: { + eventIds: [], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + match_phrase: { + 'actor.entity.id': 'admin5@example.com', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(3); + expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); + }); + + it('Should limit number of nodes', async () => { + const response = await postGraph(supertest, { + nodesLimit: 1, + query: { + eventIds: [], + start: '2024-09-01T00:00:00Z', + end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [ + { + exists: { + field: 'actor.entity.id', + }, + }, + ], + }, + }, + }, + }).expect(result(200)); + + expect(response.body).to.have.property('nodes').length(3); // Minimal number of nodes in a single relationship + expect(response.body).to.have.property('edges').length(2); + expect(response.body).to.have.property('messages').length(1); + expect(response.body.messages[0]).equal(ApiMessageCode.ReachedNodesLimit); + }); }); }); } diff --git a/x-pack/test/cloud_security_posture_api/utils.ts b/x-pack/test/cloud_security_posture_api/utils.ts index e64c583af3868..210a081b91473 100644 --- a/x-pack/test/cloud_security_posture_api/utils.ts +++ b/x-pack/test/cloud_security_posture_api/utils.ts @@ -36,22 +36,23 @@ export const waitForPluginInitialized = ({ logger.debug('CSP plugin is initialized'); }); -export function result(status: number): CallbackHandler { +export function result(status: number, logger?: ToolingLog): CallbackHandler { return (err: any, res: Response) => { if ((res?.status || err.status) !== status) { - const e = new Error( + throw new Error( `Expected ${status} ,got ${res?.status || err.status} resp: ${ res?.body ? JSON.stringify(res.body) : err.text }` ); - throw e; + } else if (err) { + logger?.warning(`Error result ${err.text}`); } }; } export class EsIndexDataProvider { private es: EsClient; - private index: string; + private readonly index: string; constructor(es: EsClient, index: string) { this.es = es; diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts b/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts index 152e3dfd4c69d..4fa0e485be2b5 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_with_streaming.ts @@ -36,7 +36,8 @@ export default function (providerContext: FtrProviderContext) { return res?._source?.['epm-packages'] as Installation; }; - describe('Installs a package using stream-based approach', () => { + // Failing: See https://github.com/elastic/kibana/issues/199701 + describe.skip('Installs a package using stream-based approach', () => { skipIfNoDockerRegistry(providerContext); before(async () => { diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts index 51b0842e60bc8..c5927d894911c 100644 --- a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -114,6 +114,34 @@ export function initRoutes( } ); + router.post( + { + path: `/api/sample_tasks/run_mark_removed_tasks_as_unrecognized`, + validate: { + body: schema.object({}), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const taskManager = await taskManagerStart; + await taskManager.ensureScheduled({ + id: 'mark_removed_tasks_as_unrecognized', + taskType: 'task_manager:mark_removed_tasks_as_unrecognized', + schedule: { interval: '1h' }, + state: {}, + params: {}, + }); + return res.ok({ body: await taskManager.runSoon('mark_removed_tasks_as_unrecognized') }); + } catch (err) { + return res.ok({ body: { id: 'mark_removed_tasks_as_unrecognized', error: `${err}` } }); + } + } + ); + router.post( { path: `/api/sample_tasks/bulk_enable`, diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts index 091e0fe01e415..c8056c2ee205e 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts @@ -168,6 +168,7 @@ export default function ({ getService }: FtrProviderContext) { 'security:telemetry-timelines', 'session_cleanup', 'task_manager:delete_inactive_background_task_nodes', + 'task_manager:mark_removed_tasks_as_unrecognized', ]); }); }); diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts index aae90a52572c7..a7447353e805a 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_management_removed_types.ts @@ -92,6 +92,12 @@ export default function ({ getService }: FtrProviderContext) { let scheduledTaskRuns = 0; let scheduledTaskInstanceRunAt = scheduledTask.runAt; + await request + .post('/api/sample_tasks/run_mark_removed_tasks_as_unrecognized') + .set('kbn-xsrf', 'xxx') + .send({}) + .expect(200); + await retry.try(async () => { const tasks = (await currentTasks()).docs; expect(tasks.length).to.eql(3); diff --git a/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts b/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts index f1e697399fe09..acdbae0b00337 100644 --- a/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts +++ b/x-pack/test/task_manager_claimer_update_by_query/plugins/sample_task_plugin_mget/server/init_routes.ts @@ -117,6 +117,34 @@ export function initRoutes( } ); + router.post( + { + path: `/api/sample_tasks/run_mark_removed_tasks_as_unrecognized`, + validate: { + body: schema.object({}), + }, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + try { + const taskManager = await taskManagerStart; + await taskManager.ensureScheduled({ + id: 'mark_removed_tasks_as_unrecognized', + taskType: 'task_manager:mark_removed_tasks_as_unrecognized', + schedule: { interval: '1h' }, + state: {}, + params: {}, + }); + return res.ok({ body: await taskManager.runSoon('mark_removed_tasks_as_unrecognized') }); + } catch (err) { + return res.ok({ body: { id: 'mark_removed_tasks_as_unrecognized', error: `${err}` } }); + } + } + ); + router.post( { path: `/api/sample_tasks/bulk_enable`, diff --git a/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts b/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts index aae90a52572c7..a7447353e805a 100644 --- a/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts +++ b/x-pack/test/task_manager_claimer_update_by_query/test_suites/task_manager/task_management_removed_types.ts @@ -92,6 +92,12 @@ export default function ({ getService }: FtrProviderContext) { let scheduledTaskRuns = 0; let scheduledTaskInstanceRunAt = scheduledTask.runAt; + await request + .post('/api/sample_tasks/run_mark_removed_tasks_as_unrecognized') + .set('kbn-xsrf', 'xxx') + .send({}) + .expect(200); + await retry.try(async () => { const tasks = (await currentTasks()).docs; expect(tasks.length).to.eql(3); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts index 741d25291e8fa..aaccdd0e9a41c 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/cloud_security_posture/graph.ts @@ -12,6 +12,7 @@ import { } from '@kbn/core-http-common'; import { result } from '@kbn/test-suites-xpack/cloud_security_posture_api/utils'; import type { Agent } from 'supertest'; +import type { GraphRequest } from '@kbn/cloud-security-posture-common/types/graph/v1'; import type { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -19,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); let supertestViewer: Pick; - const postGraph = (agent: Pick, body: any) => { + const postGraph = (agent: Pick, body: GraphRequest) => { const req = agent .post('/internal/cloud_security_posture/graph') .set(ELASTIC_HTTP_VERSION_HEADER, '1') @@ -48,7 +49,6 @@ export default function ({ getService }: FtrProviderContext) { it('should return an empty graph', async () => { const response = await postGraph(supertestViewer, { query: { - actorIds: [], eventIds: [], start: 'now-1d/d', end: 'now/d', @@ -57,20 +57,26 @@ export default function ({ getService }: FtrProviderContext) { expect(response.body).to.have.property('nodes').length(0); expect(response.body).to.have.property('edges').length(0); + expect(response.body).not.to.have.property('messages'); }); it('should return a graph with nodes and edges by actor', async () => { const response = await postGraph(supertestViewer, { query: { - actorIds: ['admin@example.com'], eventIds: [], start: '2024-09-01T00:00:00Z', end: '2024-09-02T00:00:00Z', + esQuery: { + bool: { + filter: [{ match_phrase: { 'actor.entity.id': 'admin@example.com' } }], + }, + }, }, }).expect(result(200)); expect(response.body).to.have.property('nodes').length(3); expect(response.body).to.have.property('edges').length(2); + expect(response.body).not.to.have.property('messages'); response.body.nodes.forEach((node: any) => { expect(node).to.have.property('color'); diff --git a/yarn.lock b/yarn.lock index 5faf426cf4d25..47ee0df930999 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1721,12 +1721,12 @@ "@elastic/transport" "^8.3.1" tslib "^2.4.0" -"@elastic/elasticsearch@^8.15.0": - version "8.15.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.15.0.tgz#cb29b3ae33203c545d435cf3dc4b557c8b4961d5" - integrity sha512-mG90EMdTDoT6GFSdqpUAhWK9LGuiJo6tOWqs0Usd/t15mPQDj7ZqHXfCBqNkASZpwPZpbAYVjd57S6nbUBINCg== +"@elastic/elasticsearch@^8.15.1": + version "8.15.1" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.15.1.tgz#ca294ba11ed1514bf87d4a2e253b11f6cefd8552" + integrity sha512-L3YzSaxrasMMGtcxnktiUDjS5f177L0zpHsBH+jL0LgPhdMk9xN/VKrAaYzvri86IlV5IbveA0ANV6o/BDUmhQ== dependencies: - "@elastic/transport" "^8.7.0" + "@elastic/transport" "^8.8.1" tslib "^2.4.0" "@elastic/ems-client@8.5.3": @@ -1906,10 +1906,10 @@ undici "^5.28.3" yaml "^2.2.2" -"@elastic/transport@^8.3.1", "@elastic/transport@^8.7.0": - version "8.7.0" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.7.0.tgz#006987fc5583f61c266e0b1003371e82efc7a6b5" - integrity sha512-IqXT7a8DZPJtqP2qmX1I2QKmxYyN27kvSW4g6pInESE1SuGwZDp2FxHJ6W2kwmYOJwQdAt+2aWwzXO5jHo9l4A== +"@elastic/transport@^8.3.1", "@elastic/transport@^8.8.1": + version "8.8.1" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.8.1.tgz#d64244907bccdad5626c860b492faeef12194b1f" + integrity sha512-4RQIiChwNIx3B0O+2JdmTq/Qobj6+1g2RQnSv1gt4V2SVfAYjGwOKu0ZMKEHQOXYNG6+j/Chero2G9k3/wXLEw== dependencies: "@opentelemetry/api" "1.x" debug "^4.3.4" @@ -8483,12 +8483,12 @@ require-from-string "^2.0.2" uri-js-replace "^1.0.1" -"@redocly/cli@^1.25.10": - version "1.25.10" - resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.25.10.tgz#647e33e4171d74a4f879304ba87366ac650ed83d" - integrity sha512-zoRMvSYOLzurcb3be5HLLlc5dLGICyHY8mueCbdE2DmLbFERhJJ5iiABKvNRJSr03AR6X569f4mraBJpAsGJnQ== +"@redocly/cli@^1.25.11": + version "1.25.11" + resolved "https://registry.yarnpkg.com/@redocly/cli/-/cli-1.25.11.tgz#8ec17a6535aebfd166e8cab8ffcc9d768af1b014" + integrity sha512-dttBsmLnnbTlJCTa+s7Sy+qtXDq692n7Ru3nUUIHp9XdCbhXIHWhpc8uAl+GmR4MGbVe8ohATl3J+zX3aFy82A== dependencies: - "@redocly/openapi-core" "1.25.10" + "@redocly/openapi-core" "1.25.11" abort-controller "^3.0.0" chokidar "^3.5.1" colorette "^1.2.0" @@ -8513,10 +8513,10 @@ resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.16.0.tgz#4b7700a5cb6e04bc6d6fdb94b871c9e260a1fba6" integrity sha512-t9jnODbUcuANRSl/K4L9nb12V+U5acIHnVSl26NWrtSdDZVtoqUXk2yGFPZzohYf62cCfEQUT8ouJ3bhPfpnJg== -"@redocly/openapi-core@1.25.10", "@redocly/openapi-core@^1.4.0": - version "1.25.10" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.25.10.tgz#6ca3f1ad1b826e3680f91752abf11aa40856f6b8" - integrity sha512-wcGnSonJZvjpPaJJs+qh0ADYy0aCbaNhCXhJVES9RlknMc7V9nbqLQ67lkwaXhpp/fskm9GJWL/U9Xyiuclbqw== +"@redocly/openapi-core@1.25.11", "@redocly/openapi-core@^1.4.0": + version "1.25.11" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.25.11.tgz#93f168284986da6809363b001e9aa7c2104c2fc0" + integrity sha512-bH+a8izQz4fnKROKoX3bEU8sQ9rjvEIZOqU6qTmxlhOJ0NsKa5e+LmU18SV0oFeg5YhWQhhEDihXkvKJ1wMMNQ== dependencies: "@redocly/ajv" "^8.11.2" "@redocly/config" "^0.16.0"