From a0d6a1bc9b7fceb85a71bcd936b950b3ede60356 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 3 Feb 2022 17:45:59 +0300 Subject: [PATCH 01/67] [Discover] introduce .index-threshold rule --- src/plugins/data/public/index.ts | 2 +- .../data/public/ui/filter_bar/filter_bar.tsx | 7 +- .../data/public/ui/filter_bar/index.tsx | 8 + src/plugins/data/public/ui/index.ts | 2 +- src/plugins/discover/kibana.json | 2 +- ...top_nav_links.ts => get_top_nav_links.tsx} | 41 ++ .../top_nav/open_alerts_popover.tsx | 173 +++++++++ src/plugins/discover/public/build_services.ts | 9 + src/plugins/discover/public/plugin.tsx | 2 + src/plugins/discover/tsconfig.json | 3 +- x-pack/plugins/alerting/kibana.json | 1 + x-pack/plugins/alerting/server/plugin.ts | 8 +- x-pack/plugins/alerting/server/types.ts | 2 + .../plugins/features/server/oss_features.ts | 17 + .../public/alert_types/es_query/constans.ts | 21 + .../es_query/es_query_expression.tsx | 348 +++++++++++++++++ .../alert_types/es_query/expression.tsx | 360 +----------------- .../es_query/index_threshold_expression.tsx | 221 +++++++++++ .../public/alert_types/es_query/types.ts | 3 + .../public/alert_types/es_query/validation.ts | 98 +++-- .../alert_types/es_query/action_context.ts | 2 +- .../server/alert_types/es_query/alert_type.ts | 341 +++++++++++------ .../alert_types/es_query/alert_type_params.ts | 24 +- .../server/alert_types/es_query/index.ts | 7 +- .../stack_alerts/server/alert_types/index.ts | 3 +- x-pack/plugins/stack_alerts/server/plugin.ts | 1 + 26 files changed, 1187 insertions(+), 519 deletions(-) rename src/plugins/discover/public/application/main/components/top_nav/{get_top_nav_links.ts => get_top_nav_links.tsx} (78%) create mode 100644 src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/constans.ts create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/index_threshold_expression.tsx diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 7037af5ce54b1..917c10f08c554 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -19,7 +19,7 @@ export * from './deprecated'; */ export { getEsQueryConfig } from '../common'; -export { FilterLabel, FilterItem } from './ui'; +export { FilterLabel, FilterItem, FilterBar } from './ui'; export { getDisplayValueFromFilter, generateFilters, extractTimeRange } from './query'; /** diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 9bc64eb1f6919..302bd39c920fe 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -30,7 +30,7 @@ import { IDataPluginServices, IIndexPattern } from '../..'; import { UI_SETTINGS } from '../../../common'; -interface Props { +export interface FilterBarProps { filters: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; className: string; @@ -40,7 +40,7 @@ interface Props { timeRangeForSuggestionsOverride?: boolean; } -const FilterBarUI = React.memo(function FilterBarUI(props: Props) { +const FilterBarUI = React.memo(function FilterBarUI(props: FilterBarProps) { const groupRef = useRef(null); const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); const kibana = useKibana(); @@ -229,3 +229,6 @@ const FilterBarUI = React.memo(function FilterBarUI(props: Props) { }); export const FilterBar = injectI18n(FilterBarUI); + +// eslint-disable-next-line import/no-default-export +export default FilterBar; diff --git a/src/plugins/data/public/ui/filter_bar/index.tsx b/src/plugins/data/public/ui/filter_bar/index.tsx index b3c02b2863c83..32742e83930d6 100644 --- a/src/plugins/data/public/ui/filter_bar/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/index.tsx @@ -7,6 +7,7 @@ */ import React from 'react'; +import type { FilterBarProps } from './filter_bar'; const Fallback = () =>
; @@ -23,3 +24,10 @@ export const FilterItem = (props: React.ComponentProps) = ); + +const LazyFilterBar = React.lazy(() => import('./filter_bar')); +export const FilterBar = (props: FilterBarProps) => ( + }> + + +); diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 026db1b7c09ee..2cdf68ed5ac53 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -7,7 +7,7 @@ */ export type { IndexPatternSelectProps } from './index_pattern_select'; -export { FilterLabel, FilterItem } from './filter_bar'; +export { FilterLabel, FilterItem, FilterBar } from './filter_bar'; export type { QueryStringInputProps } from './query_string_input'; export { QueryStringInput } from './query_string_input'; export type { SearchBarProps, StatefulSearchBarProps } from './search_bar'; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 015cb6ddaf285..2b09f2bef4896 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -16,7 +16,7 @@ "dataViewFieldEditor", "dataViewEditor" ], - "optionalPlugins": ["home", "share", "usageCollection", "spaces"], + "optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"], "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "dataViews"], "extraPublicDirs": ["common"], "owner": { diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx similarity index 78% rename from src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.ts rename to src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index 0a8bcae983d35..9d4b85341f7a0 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import React from 'react'; import { i18n } from '@kbn/i18n'; import type { DataView, ISearchSource } from 'src/plugins/data/common'; import { showOpenSearchPanel } from './show_open_search_panel'; @@ -17,6 +18,8 @@ import { onSaveSearch } from './on_save_search'; import { GetStateReturn } from '../../services/discover_state'; import { openOptionsPopover } from './open_options_popover'; import type { TopNavMenuData } from '../../../../../../navigation/public'; +import { openAlertsPopover } from './open_alerts_popover'; +import { toMountPoint, wrapWithTheme, MarkdownSimple } from '../../../../../../kibana_react/public'; /** * Helper function to build the top nav links @@ -58,6 +61,43 @@ export const getTopNavLinks = ({ testId: 'discoverOptionsButton', }; + const alerts = { + id: 'alerts', + label: i18n.translate('discover.localMenu.localMenu.alertsTitle', { + defaultMessage: 'Alerts', + }), + description: i18n.translate('discover.localMenu.alertsDescription', { + defaultMessage: 'Alerts', + }), + run: (anchorElement: HTMLElement) => { + if (savedSearch.searchSource.getField('index')?.timeFieldName) { + openAlertsPopover({ + I18nContext: services.core.i18n.Context, + anchorElement, + searchSource: savedSearch.searchSource, + services, + }); + } else { + services.toastNotifications.addDanger({ + title: i18n.translate('discover.alert.errorHeader', { + defaultMessage: "Cant't create alert", + }), + text: toMountPoint( + wrapWithTheme( + + {i18n.translate('discover.alert.dataViewDoesNotHaveTimeFieldErrorMessage', { + defaultMessage: 'Data view does not have time field.', + })} + , + services.core.theme.theme$ + ) + ), + }); + } + }, + testId: 'discoverAlertsButton', + }; + const newSearch = { id: 'new', label: i18n.translate('discover.localMenu.localMenu.newSearchTitle', { @@ -161,6 +201,7 @@ export const getTopNavLinks = ({ ...(services.capabilities.advancedSettings.save ? [options] : []), newSearch, openSearch, + ...(services.triggersActionsUi ? [alerts] : []), shareSearch, inspectSearch, ...(services.capabilities.discover.save ? [saveSearch] : []), diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx new file mode 100644 index 0000000000000..f3a20fb42278f --- /dev/null +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useState, useMemo } from 'react'; +import ReactDOM from 'react-dom'; +import { I18nStart } from 'kibana/public'; +import { EuiWrappingPopover, EuiLink, EuiContextMenu } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { ISearchSource } from '../../../../../../data/common'; +import { KibanaContextProvider } from '../../../../../../kibana_react/public'; +import { DiscoverServices } from '../../../../build_services'; +import { updateSearchSource } from '../../utils/update_search_source'; +import { useDiscoverServices } from '../../../../utils/use_discover_services'; + +const container = document.createElement('div'); +let isOpen = false; + +const ALERT_TYPE_ID = '.es-query'; + +interface AlertsPopoverProps { + onClose: () => void; + anchorElement: HTMLElement; + searchSource: ISearchSource; +} + +export function AlertsPopover(props: AlertsPopoverProps) { + const dataView = props.searchSource.getField('index')!; + const searchSource = props.searchSource; + const services = useDiscoverServices(); + const { triggersActionsUi } = services; + const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); + + const onCloseAlertFlyout = useCallback( + () => setAlertFlyoutVisibility(false), + [setAlertFlyoutVisibility] + ); + /** + * Provides the default parameters used to initialize the new rule + */ + const getParams = useCallback(() => { + const nextSearchSource = searchSource.createCopy(); + updateSearchSource(nextSearchSource, true, { + indexPattern: searchSource.getField('index')!, + services, + sort: [], + useNewFieldsApi: true, + }); + + return { + index: [dataView?.id], + timeField: dataView?.timeFieldName, + searchType: 'searchSource', + searchConfiguration: nextSearchSource.getSerializedFields(), + }; + }, [searchSource, services, dataView]); + + const SearchThresholdAlertFlyout = useMemo(() => { + if (!alertFlyoutVisible) { + return; + } + return triggersActionsUi?.getAddAlertFlyout({ + consumer: 'discover', + onClose: onCloseAlertFlyout, + canChangeTrigger: false, + alertTypeId: ALERT_TYPE_ID, + initialValues: { + params: getParams(), + }, + }); + }, [getParams, onCloseAlertFlyout, triggersActionsUi, alertFlyoutVisible]); + + const panels = [ + { + id: 'mainPanel', + name: 'Alerting', + items: [ + { + name: ( + <> + {SearchThresholdAlertFlyout} + { + setAlertFlyoutVisibility(true); + }} + > + + + + ), + icon: 'bell', + disabled: !dataView.timeFieldName, + }, + { + name: ( + + + + ), + icon: 'tableOfContents', + }, + ], + }, + ]; + + return ( + <> + {SearchThresholdAlertFlyout} + + + + + ); +} + +function onClose() { + ReactDOM.unmountComponentAtNode(container); + document.body.removeChild(container); + isOpen = false; +} + +export function openAlertsPopover({ + I18nContext, + anchorElement, + searchSource, + services, +}: { + I18nContext: I18nStart['Context']; + anchorElement: HTMLElement; + searchSource: ISearchSource; + services: DiscoverServices; +}) { + if (isOpen) { + onClose(); + return; + } + + isOpen = true; + document.body.appendChild(container); + + const element = ( + + + + + + ); + ReactDOM.render(element, container); +} diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index f6492db6e8a42..3d4f74c6d9ffc 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -18,6 +18,8 @@ import { IUiSettingsClient, PluginInitializerContext, HttpStart, + NotificationsStart, + ApplicationStart, } from 'kibana/public'; import { FilterManager, @@ -41,12 +43,14 @@ import { EmbeddableStart } from '../../embeddable/public'; import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public'; import { DataViewEditorStart } from '../../../plugins/data_view_editor/public'; +import type { TriggersAndActionsUIPublicPluginStart } from '../../../../x-pack/plugins/triggers_actions_ui/public'; export interface HistoryLocationState { referrer: string; } export interface DiscoverServices { + application: ApplicationStart; addBasePath: (path: string) => string; capabilities: Capabilities; chrome: ChromeStart; @@ -66,6 +70,7 @@ export interface DiscoverServices { urlForwarding: UrlForwardingStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; + notifications: NotificationsStart; uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; dataViewFieldEditor: IndexPatternFieldEditorStart; @@ -73,6 +78,7 @@ export interface DiscoverServices { http: HttpStart; storage: Storage; spaces?: SpacesApi; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } export const buildServices = memoize(function ( @@ -84,6 +90,7 @@ export const buildServices = memoize(function ( const storage = new Storage(localStorage); return { + application: core.application, addBasePath: core.http.basePath.prepend, capabilities: core.application.capabilities, chrome: core.chrome, @@ -105,6 +112,7 @@ export const buildServices = memoize(function ( urlForwarding: plugins.urlForwarding, timefilter: plugins.data.query.timefilter.timefilter, toastNotifications: core.notifications.toasts, + notifications: core.notifications, uiSettings: core.uiSettings, storage, trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'), @@ -112,5 +120,6 @@ export const buildServices = memoize(function ( http: core.http, spaces: plugins.spaces, dataViewEditor: plugins.dataViewEditor, + triggersActionsUi: plugins.triggersActionsUi, }; }); diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index e55158b0dad5e..c9219a97fbd3e 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -64,6 +64,7 @@ import { injectTruncateStyles } from './utils/truncate_styles'; import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '../common'; import { DataViewEditorStart } from '../../../plugins/data_view_editor/public'; import { useDiscoverServices } from './utils/use_discover_services'; +import type { TriggersAndActionsUIPublicPluginStart } from '../../../../x-pack/plugins/triggers_actions_ui/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -191,6 +192,7 @@ export interface DiscoverStartPlugins { usageCollection?: UsageCollectionSetup; dataViewFieldEditor: IndexPatternFieldEditorStart; spaces?: SpacesPluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } /** diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 6dad573a272fb..817e73f16617e 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../field_formats/tsconfig.json" }, { "path": "../data_views/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, - { "path": "../data_view_editor/tsconfig.json" } + { "path": "../data_view_editor/tsconfig.json" }, + { "path": "../../../x-pack/plugins/triggers_actions_ui/tsconfig.json" } ] } diff --git a/x-pack/plugins/alerting/kibana.json b/x-pack/plugins/alerting/kibana.json index 82d8de0daf14a..35bfc527fe737 100644 --- a/x-pack/plugins/alerting/kibana.json +++ b/x-pack/plugins/alerting/kibana.json @@ -11,6 +11,7 @@ "configPath": ["xpack", "alerting"], "requiredPlugins": [ "actions", + "data", "encryptedSavedObjects", "eventLog", "features", diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 70aad0d6921e1..707cda567d809 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -66,6 +66,7 @@ import { getHealth } from './health/get_health'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; import { AlertingAuthorization } from './authorization'; import { getSecurityHealth, SecurityHealth } from './lib/get_security_health'; +import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -136,6 +137,7 @@ export interface AlertingPluginsStart { licensing: LicensingPluginStart; spaces?: SpacesPluginStart; security?: SecurityPluginStart; + data: DataPluginStart; } export class AlertingPlugin { @@ -380,7 +382,7 @@ export class AlertingPlugin { this.config.then((config) => { taskRunnerFactory.initialize({ logger, - getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), + getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch, plugins.data), getRulesClientWithRequest, spaceIdToNamespace, actionsPlugin: plugins.actions, @@ -445,11 +447,13 @@ export class AlertingPlugin { private getServicesFactory( savedObjects: SavedObjectsServiceStart, - elasticsearch: ElasticsearchServiceStart + elasticsearch: ElasticsearchServiceStart, + data: DataPluginStart ): (request: KibanaRequest) => Services { return (request) => ({ savedObjectsClient: this.getScopedClientWithAlertSavedObjectType(savedObjects, request), scopedClusterClient: elasticsearch.client.asScoped(request), + searchSourceClient: data.search.searchSource.asScoped(request), }); } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index de6649bb44891..8ff39af6ab6c6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -37,6 +37,7 @@ import { } from '../common'; import { LicenseType } from '../../licensing/server'; import { IAbortableClusterClient } from './lib/create_abortable_es_client_factory'; +import { ISearchStartSearchSource } from '../../../../src/plugins/data/common'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; @@ -67,6 +68,7 @@ export type AlertingRouter = IRouter; export interface Services { savedObjectsClient: SavedObjectsClientContract; scopedClusterClient: IScopedClusterClient; + searchSourceClient: Promise; } export interface AlertServices< diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 0c240d541f80e..4b9bd9cf15e39 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -32,6 +32,7 @@ export const buildOSSFeatures = ({ category: DEFAULT_APP_CATEGORIES.kibana, app: ['discover', 'kibana'], catalogue: ['discover'], + alerting: ['.es-query'], privileges: { all: { app: ['discover', 'kibana'], @@ -42,6 +43,14 @@ export const buildOSSFeatures = ({ read: ['index-pattern'], }, ui: ['show', 'save', 'saveQuery'], + alerting: { + rule: { + all: ['.es-query'], + }, + alert: { + all: ['.es-query'], + }, + }, }, read: { app: ['discover', 'kibana'], @@ -51,6 +60,14 @@ export const buildOSSFeatures = ({ read: ['index-pattern', 'search', 'query'], }, ui: ['show'], + alerting: { + rule: { + all: ['.es-query'], + }, + alert: { + all: ['.es-query'], + }, + }, }, }, subFeatures: [ diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/constans.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/constans.ts new file mode 100644 index 0000000000000..bd2de94ce1e96 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/constans.ts @@ -0,0 +1,21 @@ +/* + * 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 { COMPARATORS } from '../../../../triggers_actions_ui/public'; + +export const DEFAULT_VALUES = { + THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, + QUERY: `{ + "query":{ + "match_all" : {} + } + }`, + SIZE: 100, + TIME_WINDOW_SIZE: 5, + TIME_WINDOW_UNIT: 'm', + THRESHOLD: [1000], +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx new file mode 100644 index 0000000000000..7e6fccef14825 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx @@ -0,0 +1,348 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, Fragment, useEffect, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { XJsonMode } from '@kbn/ace'; +import 'brace/theme/github'; + +import { EuiButtonEmpty, EuiSpacer, EuiFormRow, EuiTitle, EuiLink, EuiText } from '@elastic/eui'; +import { DocLinksStart, HttpSetup } from 'kibana/public'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { XJson, EuiCodeEditor } from '../../../../../../src/plugins/es_ui_shared/public'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { + getFields, + ValueExpression, + RuleTypeParamsExpressionProps, + ForLastExpression, + ThresholdExpression, +} from '../../../../triggers_actions_ui/public'; +import { validateExpression } from './validation'; +import { parseDuration } from '../../../../alerting/common'; +import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; +import { EsQueryAlertParams } from './types'; +import { IndexSelectPopover } from '../components/index_select_popover'; +import { DEFAULT_VALUES } from './constans'; + +function totalHitsToNumber(total: estypes.SearchHitsMetadata['total']): number { + return typeof total === 'number' ? total : total?.value ?? 0; +} + +const { useXJsonMode } = XJson; +const xJsonMode = new XJsonMode(); + +interface KibanaDeps { + http: HttpSetup; + docLinks: DocLinksStart; +} + +export const EsQueryExpression = ({ + ruleParams, + setRuleParams, + setRuleProperty, + errors, + data, +}: RuleTypeParamsExpressionProps) => { + const { + index, + timeField, + esQuery, + size, + thresholdComparator, + threshold, + timeWindowSize, + timeWindowUnit, + } = ruleParams; + + const getDefaultParams = () => { + return { + ...ruleParams, + timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, + thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, + size: size ?? DEFAULT_VALUES.SIZE, + searchType: 'esQuery', + esQuery: esQuery ?? DEFAULT_VALUES.QUERY, + }; + }; + + const [currentAlertParams, setCurrentAlertParams] = useState( + getDefaultParams() + ); + + const setParam = useCallback( + (paramField: string, paramValue: unknown) => { + setCurrentAlertParams((currentParams) => ({ + ...currentParams, + [paramField]: paramValue, + })); + setRuleParams(paramField, paramValue); + }, + [setRuleParams] + ); + + const { http, docLinks } = useKibana().services; + + const [esFields, setEsFields] = useState< + Array<{ + name: string; + type: string; + normalizedType: string; + searchable: boolean; + aggregatable: boolean; + }> + >([]); + const { convertToJson, setXJson, xJson } = useXJsonMode(DEFAULT_VALUES.QUERY); + const [testQueryResult, setTestQueryResult] = useState(null); + const [testQueryError, setTestQueryError] = useState(null); + + const setDefaultExpressionValues = async () => { + setRuleProperty('params', currentAlertParams); + setXJson(esQuery ?? DEFAULT_VALUES.QUERY); + + if (index && index.length > 0) { + await refreshEsFields(); + } + }; + + useEffect(() => { + setDefaultExpressionValues(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const refreshEsFields = async () => { + if (index) { + const currentEsFields = await getFields(http, index); + setEsFields(currentEsFields); + } + }; + + const hasValidationErrors = () => { + const { errors: validationErrors } = validateExpression(currentAlertParams); + return Object.keys(validationErrors).some( + (key) => validationErrors[key] && validationErrors[key].length + ); + }; + + const onTestQuery = async () => { + if (!hasValidationErrors()) { + setTestQueryError(null); + setTestQueryResult(null); + try { + const window = `${timeWindowSize}${timeWindowUnit}`; + const timeWindow = parseDuration(window); + const parsedQuery = JSON.parse(esQuery); + const now = Date.now(); + const { rawResponse } = await data.search + .search({ + params: buildSortedEventsQuery({ + index, + from: new Date(now - timeWindow).toISOString(), + to: new Date(now).toISOString(), + filter: parsedQuery.query, + size: 0, + searchAfterSortId: undefined, + timeField: timeField ? timeField : '', + track_total_hits: true, + }), + }) + .toPromise(); + + const hits = rawResponse.hits; + setTestQueryResult( + i18n.translate('xpack.stackAlerts.esQuery.ui.numQueryMatchesText', { + defaultMessage: 'Query matched {count} documents in the last {window}.', + values: { count: totalHitsToNumber(hits.total), window }, + }) + ); + } catch (err) { + const message = err?.body?.attributes?.error?.root_cause[0]?.reason || err?.body?.message; + setTestQueryError( + i18n.translate('xpack.stackAlerts.esQuery.ui.queryError', { + defaultMessage: 'Error testing query: {message}', + values: { message: message ? `${err.message}: ${message}` : err.message }, + }) + ); + } + } + }; + + return ( + + +
+ +
+
+ + { + setParam('index', indices); + + // reset expression fields if indices are deleted + if (indices.length === 0) { + setRuleProperty('params', { + ...ruleParams, + index: indices, + esQuery: DEFAULT_VALUES.QUERY, + size: DEFAULT_VALUES.SIZE, + thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, + timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: DEFAULT_VALUES.THRESHOLD, + timeField: '', + }); + } else { + await refreshEsFields(); + } + }} + onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)} + /> + { + setParam('size', updatedValue); + }} + /> + + +
+ +
+
+ + + } + isInvalid={errors.esQuery.length > 0} + error={errors.esQuery} + helpText={ + + + + } + > + { + setXJson(xjson); + setParam('esQuery', convertToJson(xjson)); + }} + /> + + + + + + + {testQueryResult && ( + + +

{testQueryResult}

+
+
+ )} + {testQueryError && ( + + +

{testQueryError}

+
+
+ )} + + +
+ +
+
+ + + setParam('threshold', selectedThresholds) + } + onChangeSelectedThresholdComparator={(selectedThresholdComparator) => + setParam('thresholdComparator', selectedThresholdComparator) + } + /> + + setParam('timeWindowSize', selectedWindowSize) + } + onChangeWindowUnit={(selectedWindowUnit: string) => + setParam('timeWindowUnit', selectedWindowUnit) + } + /> + +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index a625d9c193cd6..5820a5661612b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -5,57 +5,16 @@ * 2.0. */ -import React, { useState, Fragment, useEffect } from 'react'; +import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { XJsonMode } from '@kbn/ace'; import 'brace/theme/github'; -import { - EuiButtonEmpty, - EuiSpacer, - EuiFormRow, - EuiCallOut, - EuiText, - EuiTitle, - EuiLink, -} from '@elastic/eui'; -import { DocLinksStart, HttpSetup } from 'kibana/public'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; - -import { XJson, EuiCodeEditor } from '../../../../../../src/plugins/es_ui_shared/public'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { - getFields, - COMPARATORS, - ThresholdExpression, - ForLastExpression, - ValueExpression, - RuleTypeParamsExpressionProps, -} from '../../../../triggers_actions_ui/public'; -import { validateExpression } from './validation'; -import { parseDuration } from '../../../../alerting/common'; -import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { RuleTypeParamsExpressionProps } from '../../../../triggers_actions_ui/public'; import { EsQueryAlertParams } from './types'; -import { IndexSelectPopover } from '../components/index_select_popover'; - -function totalHitsToNumber(total: estypes.SearchHitsMetadata['total']): number { - return typeof total === 'number' ? total : total?.value ?? 0; -} - -const DEFAULT_VALUES = { - THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, - QUERY: `{ - "query":{ - "match_all" : {} - } -}`, - SIZE: 100, - TIME_WINDOW_SIZE: 5, - TIME_WINDOW_UNIT: 'm', - THRESHOLD: [1000], -}; +import { IndexThresholdParameters } from './index_threshold_expression'; +import { EsQueryExpression } from './es_query_expression'; const expressionFieldsWithValidation = [ 'index', @@ -67,62 +26,24 @@ const expressionFieldsWithValidation = [ 'timeWindowSize', ]; -const { useXJsonMode } = XJson; -const xJsonMode = new XJsonMode(); - -interface KibanaDeps { - http: HttpSetup; - docLinks: DocLinksStart; -} - export const EsQueryAlertTypeExpression: React.FunctionComponent< RuleTypeParamsExpressionProps -> = ({ ruleParams, setRuleParams, setRuleProperty, errors, data }) => { +> = (props) => { const { - index, - timeField, - esQuery, - size, - thresholdComparator, - threshold, - timeWindowSize, - timeWindowUnit, - } = ruleParams; - - const getDefaultParams = () => ({ - ...ruleParams, - esQuery: esQuery ?? DEFAULT_VALUES.QUERY, - size: size ?? DEFAULT_VALUES.SIZE, - timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, - timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, - threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, - thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, - }); - - const { http, docLinks } = useKibana().services; + ruleParams: { searchType }, + ruleParams, + errors, + } = props; - const [esFields, setEsFields] = useState< - Array<{ - name: string; - type: string; - normalizedType: string; - searchable: boolean; - aggregatable: boolean; - }> - >([]); - const { convertToJson, setXJson, xJson } = useXJsonMode(DEFAULT_VALUES.QUERY); - const [currentAlertParams, setCurrentAlertParams] = useState( - getDefaultParams() - ); - const [testQueryResult, setTestQueryResult] = useState(null); - const [testQueryError, setTestQueryError] = useState(null); + const isIndexThreshold = searchType === 'searchSource'; - const hasExpressionErrors = !!Object.keys(errors).find( - (errorKey) => + const hasExpressionErrors = !!Object.keys(errors).find((errorKey) => { + return ( expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1 && ruleParams[errorKey as keyof EsQueryAlertParams] !== undefined - ); + ); + }); const expressionErrorMessage = i18n.translate( 'xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage', @@ -131,260 +52,21 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< } ); - const setDefaultExpressionValues = async () => { - setRuleProperty('params', getDefaultParams()); - - setXJson(esQuery ?? DEFAULT_VALUES.QUERY); - - if (index && index.length > 0) { - await refreshEsFields(); - } - }; - - const setParam = (paramField: string, paramValue: unknown) => { - setCurrentAlertParams({ - ...currentAlertParams, - [paramField]: paramValue, - }); - setRuleParams(paramField, paramValue); - }; - - useEffect(() => { - setDefaultExpressionValues(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const refreshEsFields = async () => { - if (index) { - const currentEsFields = await getFields(http, index); - setEsFields(currentEsFields); - } - }; - - const hasValidationErrors = () => { - const { errors: validationErrors } = validateExpression(currentAlertParams); - return Object.keys(validationErrors).some( - (key) => validationErrors[key] && validationErrors[key].length - ); - }; - - const onTestQuery = async () => { - if (!hasValidationErrors()) { - setTestQueryError(null); - setTestQueryResult(null); - try { - const window = `${timeWindowSize}${timeWindowUnit}`; - const timeWindow = parseDuration(window); - const parsedQuery = JSON.parse(esQuery); - const now = Date.now(); - const { rawResponse } = await data.search - .search({ - params: buildSortedEventsQuery({ - index, - from: new Date(now - timeWindow).toISOString(), - to: new Date(now).toISOString(), - filter: parsedQuery.query, - size: 0, - searchAfterSortId: undefined, - timeField: timeField ? timeField : '', - track_total_hits: true, - }), - }) - .toPromise(); - - const hits = rawResponse.hits; - setTestQueryResult( - i18n.translate('xpack.stackAlerts.esQuery.ui.numQueryMatchesText', { - defaultMessage: 'Query matched {count} documents in the last {window}.', - values: { count: totalHitsToNumber(hits.total), window }, - }) - ); - } catch (err) { - const message = err?.body?.attributes?.error?.root_cause[0]?.reason || err?.body?.message; - setTestQueryError( - i18n.translate('xpack.stackAlerts.esQuery.ui.queryError', { - defaultMessage: 'Error testing query: {message}', - values: { message: message ? `${err.message}: ${message}` : err.message }, - }) - ); - } - } - }; - return ( - {hasExpressionErrors ? ( + {hasExpressionErrors && ( - ) : null} - -
- -
-
- - { - setParam('index', indices); - - // reset expression fields if indices are deleted - if (indices.length === 0) { - setRuleProperty('params', { - ...ruleParams, - index: indices, - esQuery: DEFAULT_VALUES.QUERY, - size: DEFAULT_VALUES.SIZE, - thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, - timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE, - timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT, - threshold: DEFAULT_VALUES.THRESHOLD, - timeField: '', - }); - } else { - await refreshEsFields(); - } - }} - onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)} - /> - { - setParam('size', updatedValue); - }} - /> - - -
- -
-
- - - } - isInvalid={errors.esQuery.length > 0} - error={errors.esQuery} - helpText={ - - - - } - > - { - setXJson(xjson); - setParam('esQuery', convertToJson(xjson)); - }} - /> - - - - - - - {testQueryResult && ( - - -

{testQueryResult}

-
-
)} - {testQueryError && ( - - -

{testQueryError}

-
-
+ + {isIndexThreshold ? ( + + ) : ( + )} - - -
- -
-
- - - setParam('threshold', selectedThresholds) - } - onChangeSelectedThresholdComparator={(selectedThresholdComparator) => - setParam('thresholdComparator', selectedThresholdComparator) - } - /> - - setParam('timeWindowSize', selectedWindowSize) - } - onChangeWindowUnit={(selectedWindowUnit: string) => - setParam('timeWindowUnit', selectedWindowUnit) - } - /> -
); }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index_threshold_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index_threshold_expression.tsx new file mode 100644 index 0000000000000..bf8a44ed7db95 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index_threshold_expression.tsx @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, useCallback, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiSpacer, EuiTitle, EuiExpression, EuiPopover, EuiText } from '@elastic/eui'; +import { Filter, ISearchSource } from '../../../../../../src/plugins/data/common'; +import { QueryStringInput } from '../../../../../../src/plugins/data/public'; +import { FilterBar } from '../../../../../../src/plugins/data/public'; +import { EsQueryAlertParams } from './types'; +import { + ForLastExpression, + RuleTypeParamsExpressionProps, + ThresholdExpression, +} from '../../../../triggers_actions_ui/public'; +import { DEFAULT_VALUES } from './constans'; + +export const IndexThresholdParameters = ({ + ruleParams, + setRuleParams, + setRuleProperty, + data, + errors, +}: RuleTypeParamsExpressionProps) => { + const { thresholdComparator, threshold, timeWindowSize, timeWindowUnit } = ruleParams; + + const getDefaultParams = () => { + const defaults = { + timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, + thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, + }; + + return { ...ruleParams, ...defaults, searchType: 'searchSource' }; + }; + + const [currentAlertParams, setCurrentAlertParams] = useState( + getDefaultParams() + ); + + const setParam = useCallback( + (paramField: string, paramValue: unknown) => { + setCurrentAlertParams((currentParams) => ({ + ...currentParams, + [paramField]: paramValue, + })); + setRuleParams(paramField, paramValue); + }, + [setRuleParams] + ); + + useEffect(() => { + setRuleProperty('params', currentAlertParams); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [usedSearchSource, setUsedSearchSource] = useState(); + const editable = false; + + const { searchConfiguration } = ruleParams; + + // Note that this PR contains a limited way to edit query and filter + // But it's out of scope for the MVP + const [showQueryBar, setShowQueryBar] = useState(false); + const [showFilter, setShowFilter] = useState(false); + + useEffect(() => { + async function initSearchSource() { + const loadedSearchSource = await data.search.searchSource.create(searchConfiguration); + setUsedSearchSource(loadedSearchSource); + } + if (searchConfiguration) { + initSearchSource(); + } + }, [data.search.searchSource, searchConfiguration]); + + if (!usedSearchSource) { + // there should be a loading indicator + return null; + } + + const filterArr = usedSearchSource.getField('filter') as Filter[]; + + return ( + + +
+ +
+
+ + + { + if (editable) { + setShowQueryBar(!showQueryBar); + } + }} + /> + } + display="block" + isOpen={showQueryBar} + closePopover={() => setShowQueryBar(false)} + > + { + usedSearchSource.setField('query', query); + setUsedSearchSource(usedSearchSource.createCopy()); + }} + /> + + + { + // currently it's just displaying the filter key + // but of course this needs to be improved + return filter.meta.key; + }) + .join(', ') + : '' + } + isActive={true} + display="columns" + onClick={() => { + if (editable) { + setShowFilter(!showFilter); + } + }} + /> + } + display="block" + isOpen={showFilter} + closePopover={() => setShowFilter(false)} + > + { + usedSearchSource.setField('filter', filters); + setUsedSearchSource(usedSearchSource.createCopy()); + }} + /> + + + + + + +
+ +
+
+ + + setParam('threshold', selectedThresholds) + } + onChangeSelectedThresholdComparator={(selectedThresholdComparator) => + setParam('thresholdComparator', selectedThresholdComparator) + } + /> + + setParam('timeWindowSize', selectedWindowSize) + } + onChangeWindowUnit={(selectedWindowUnit: string) => + setParam('timeWindowUnit', selectedWindowUnit) + } + /> + +
+ ); +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts index 826d9b25a5394..2add72ac79cf6 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts @@ -6,6 +6,7 @@ */ import { AlertTypeParams } from '../../../../alerting/common'; +import { SerializedSearchSourceFields } from '../../../../../../src/plugins/data/common'; export interface Comparator { text: string; @@ -22,4 +23,6 @@ export interface EsQueryAlertParams extends AlertTypeParams { threshold: number[]; timeWindowSize: number; timeWindowUnit: string; + searchType: string; + searchConfiguration: SerializedSearchSourceFields; } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts index d4ab8801fcdde..c2eb5a4536b04 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts @@ -10,8 +10,16 @@ import { EsQueryAlertParams } from './types'; import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public'; export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => { - const { index, timeField, esQuery, size, threshold, timeWindowSize, thresholdComparator } = - alertParams; + const { + index, + timeField, + esQuery, + size, + threshold, + timeWindowSize, + thresholdComparator, + searchType, + } = alertParams; const validationResult = { errors: {} }; const errors = { index: new Array(), @@ -24,44 +32,6 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR timeWindowSize: new Array(), }; validationResult.errors = errors; - if (!index || index.length === 0) { - errors.index.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredIndexText', { - defaultMessage: 'Index is required.', - }) - ); - } - if (!timeField) { - errors.timeField.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredTimeFieldText', { - defaultMessage: 'Time field is required.', - }) - ); - } - if (!esQuery) { - errors.esQuery.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredQueryText', { - defaultMessage: 'Elasticsearch query is required.', - }) - ); - } else { - try { - const parsedQuery = JSON.parse(esQuery); - if (!parsedQuery.query) { - errors.esQuery.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredEsQueryText', { - defaultMessage: `Query field is required.`, - }) - ); - } - } catch (err) { - errors.esQuery.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.jsonQueryText', { - defaultMessage: 'Query must be valid JSON.', - }) - ); - } - } if (!threshold || threshold.length === 0 || threshold[0] === undefined) { errors.threshold0.push( i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredThreshold0Text', { @@ -96,6 +66,54 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR }) ); } + if (!timeField) { + errors.timeField.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredTimeFieldText', { + defaultMessage: 'Time field is required.', + }) + ); + } + + /** + * Skip esQuery, size, timeField, index checks if it is .index-threshold, + * since it should contain searchConfiguration + */ + if (searchType === 'searchSource') { + return validationResult; + } + + if (!index || index.length === 0) { + errors.index.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredIndexText', { + defaultMessage: 'Index is required.', + }) + ); + } + + if (!esQuery) { + errors.esQuery.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredQueryText', { + defaultMessage: 'Elasticsearch query is required.', + }) + ); + } else { + try { + const parsedQuery = JSON.parse(esQuery); + if (!parsedQuery.query) { + errors.esQuery.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredEsQueryText', { + defaultMessage: `Query field is required.`, + }) + ); + } + } catch (err) { + errors.esQuery.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.jsonQueryText', { + defaultMessage: 'Query must be valid JSON.', + }) + ); + } + } if (!size) { errors.size.push( i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index f4886e3c055a2..74c3e53e95c00 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -29,7 +29,7 @@ export interface EsQueryAlertActionContext extends AlertInstanceContext { // threshold conditions conditions: string; // query matches - hits: estypes.SearchHit[]; + hits?: estypes.SearchHit[]; } export function addMessages( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 9dca9e9c3fc61..1ac6ca3ca1698 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Logger } from 'src/core/server'; +import { sha256 } from 'js-sha256'; +import { CoreSetup, Logger } from 'src/core/server'; import { RuleType, AlertExecutorOptions } from '../../types'; import { ActionContext, EsQueryAlertActionContext, addMessages } from './action_context'; import { @@ -19,13 +20,25 @@ import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { ComparatorFns, getHumanReadableComparator } from '../lib'; import { parseDuration } from '../../../../alerting/server'; import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; +import { getTime } from '../../../../../../src/plugins/data/common'; export const ES_QUERY_ID = '.es-query'; export const ActionGroupId = 'query matched'; export const ConditionMetAlertInstanceId = 'query matched'; -export function getAlertType(logger: Logger): RuleType< +type ExecutorOptions = AlertExecutorOptions< + EsQueryAlertParams, + EsQueryAlertState, + {}, + ActionContext, + typeof ActionGroupId +>; + +export function getAlertType( + logger: Logger, + core: CoreSetup +): RuleType< EsQueryAlertParams, never, // Only use if defining useSavedObjectReferences hook EsQueryAlertState, @@ -150,129 +163,12 @@ export function getAlertType(logger: Logger): RuleType< producer: STACK_ALERTS_FEATURE_ID, }; - async function executor( - options: AlertExecutorOptions< - EsQueryAlertParams, - EsQueryAlertState, - {}, - ActionContext, - typeof ActionGroupId - > - ) { - const { alertId, name, services, params, state } = options; - const { alertInstanceFactory, search } = services; - const previousTimestamp = state.latestTimestamp; - - const abortableEsClient = search.asCurrentUser; - const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); - - const compareFn = ComparatorFns.get(params.thresholdComparator); - if (compareFn == null) { - throw new Error(getInvalidComparatorError(params.thresholdComparator)); + async function executor(options: ExecutorOptions) { + if (options.params.searchType === 'searchSource') { + return await indexThresholdExpressionExecutor(logger, core, options); + } else { + return await esQueryExpressionExecutor(logger, options); } - - // During each alert execution, we run the configured query, get a hit count - // (hits.total) and retrieve up to params.size hits. We - // evaluate the threshold condition using the value of hits.total. If the threshold - // condition is met, the hits are counted toward the query match and we update - // the alert state with the timestamp of the latest hit. In the next execution - // of the alert, the latestTimestamp will be used to gate the query in order to - // avoid counting a document multiple times. - - let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); - const filter = timestamp - ? { - bool: { - filter: [ - parsedQuery.query, - { - bool: { - must_not: [ - { - bool: { - filter: [ - { - range: { - [params.timeField]: { - lte: timestamp, - format: 'strict_date_optional_time', - }, - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - } - : parsedQuery.query; - - const query = buildSortedEventsQuery({ - index: params.index, - from: dateStart, - to: dateEnd, - filter, - size: params.size, - sortOrder: 'desc', - searchAfterSortId: undefined, - timeField: params.timeField, - track_total_hits: true, - }); - - logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`); - - const { body: searchResult } = await abortableEsClient.search(query); - - logger.debug( - `alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` - ); - - const numMatches = (searchResult.hits.total as estypes.SearchTotalHits).value; - - // apply the alert condition - const conditionMet = compareFn(numMatches, params.threshold); - - if (conditionMet) { - const humanFn = i18n.translate( - 'xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', - { - defaultMessage: `Number of matching documents is {thresholdComparator} {threshold}`, - values: { - thresholdComparator: getHumanReadableComparator(params.thresholdComparator), - threshold: params.threshold.join(' and '), - }, - } - ); - - const baseContext: EsQueryAlertActionContext = { - date: new Date().toISOString(), - value: numMatches, - conditions: humanFn, - hits: searchResult.hits.hits, - }; - - const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertInstanceFactory(ConditionMetAlertInstanceId); - alertInstance - // store the params we would need to recreate the query that led to this alert instance - .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) - .scheduleActions(ActionGroupId, actionContext); - - // update the timestamp based on the current search results - const firstValidTimefieldSort = getValidTimefieldSort( - searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort - ); - if (firstValidTimefieldSort) { - timestamp = firstValidTimefieldSort; - } - } - - return { - latestTimestamp: timestamp, - }; } } @@ -346,3 +242,200 @@ function getSearchParams(queryParams: EsQueryAlertParams) { return { parsedQuery, dateStart, dateEnd }; } + +async function esQueryExpressionExecutor(logger: Logger, options: ExecutorOptions) { + const { alertId, name, services, params, state } = options; + const { alertInstanceFactory, search } = services; + const previousTimestamp = state.latestTimestamp; + + const abortableEsClient = search.asCurrentUser; + const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); + + const compareFn = ComparatorFns.get(params.thresholdComparator); + if (compareFn == null) { + throw new Error(getInvalidComparatorError(params.thresholdComparator)); + } + + // During each alert execution, we run the configured query, get a hit count + // (hits.total) and retrieve up to params.size hits. We + // evaluate the threshold condition using the value of hits.total. If the threshold + // condition is met, the hits are counted toward the query match and we update + // the alert state with the timestamp of the latest hit. In the next execution + // of the alert, the latestTimestamp will be used to gate the query in order to + // avoid counting a document multiple times. + + let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); + const filter = timestamp + ? { + bool: { + filter: [ + parsedQuery.query, + { + bool: { + must_not: [ + { + bool: { + filter: [ + { + range: { + [params.timeField]: { + lte: timestamp, + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + } + : parsedQuery.query; + + const query = buildSortedEventsQuery({ + index: params.index, + from: dateStart, + to: dateEnd, + filter, + size: params.size, + sortOrder: 'desc', + searchAfterSortId: undefined, + timeField: params.timeField, + track_total_hits: true, + }); + + logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`); + + const { body: searchResult } = await abortableEsClient.search(query); + + logger.debug( + `alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` + ); + + const numMatches = (searchResult.hits.total as estypes.SearchTotalHits).value; + + // apply the alert condition + const conditionMet = compareFn(numMatches, params.threshold); + + if (conditionMet) { + const humanFn = i18n.translate( + 'xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', + { + defaultMessage: `Number of matching documents is {thresholdComparator} {threshold}`, + values: { + thresholdComparator: getHumanReadableComparator(params.thresholdComparator), + threshold: params.threshold.join(' and '), + }, + } + ); + + const baseContext: EsQueryAlertActionContext = { + date: new Date().toISOString(), + value: numMatches, + conditions: humanFn, + hits: searchResult.hits.hits, + }; + + const actionContext = addMessages(options, baseContext, params); + const alertInstance = alertInstanceFactory(ConditionMetAlertInstanceId); + alertInstance + // store the params we would need to recreate the query that led to this alert instance + .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) + .scheduleActions(ActionGroupId, actionContext); + + // update the timestamp based on the current search results + const firstValidTimefieldSort = getValidTimefieldSort( + searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort + ); + if (firstValidTimefieldSort) { + timestamp = firstValidTimefieldSort; + } + } + + return { + latestTimestamp: timestamp, + }; +} + +async function indexThresholdExpressionExecutor( + logger: Logger, + core: CoreSetup, + options: ExecutorOptions +) { + const { name, params, alertId, state, services } = options; + const { timeField } = params; + const timestamp = new Date().toISOString(); + const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; + logger.debug( + `searchThreshold (${alertId}) previousTimestamp: ${state.previousTimestamp}, previousTimeRange ${state.previousTimeRange}` + ); + const compareFn = ComparatorFns.get(params.thresholdComparator); + if (compareFn == null) { + throw new Error( + i18n.translate('xpack.stackAlerts.searchThreshold.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator: params.thresholdComparator, + }, + }) + ); + } + + const searchSourceClient = await services.searchSourceClient; + const loadedSearchSource = await searchSourceClient.create(params.searchConfiguration); + const index = loadedSearchSource.getField('index'); + + loadedSearchSource.setField('size', 0); + + const filter = getTime(index, { + from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, + to: 'now', + }); + const from = filter?.query.range[timeField].gte; + const to = filter?.query.range[timeField].lte; + const searchSourceChild = loadedSearchSource.createChild(); + searchSourceChild.setField('filter', filter); + + let nrOfDocs = 0; + try { + logger.info( + `searchThreshold (${alertId}) query: ${JSON.stringify( + searchSourceChild.getSearchRequestBody() + )}` + ); + const docs = await searchSourceChild.fetch(); + nrOfDocs = Number(docs.hits.total); + logger.info(`searchThreshold (${alertId}) nrOfDocs: ${nrOfDocs}`); + } catch (error) { + logger.error('Error fetching documents: ' + error.message); + throw error; + } + + const met = compareFn(nrOfDocs, params.threshold); + + if (met) { + const conditions = `${nrOfDocs} is ${getHumanReadableComparator(params.thresholdComparator)} ${ + params.threshold + }`; + const checksum = sha256.create().update(JSON.stringify(params)); + const link = `${publicBaseUrl}/app/discover#/viewAlert/${alertId}?from=${from}&to=${to}&checksum=${checksum}`; + const baseContext: ActionContext = { + title: name, + message: `${nrOfDocs} documents found between ${from} and ${to}`, + date: timestamp, + value: Number(nrOfDocs), + conditions, + link, + }; + const alertInstance = options.services.alertInstanceFactory(ConditionMetAlertInstanceId); + alertInstance.scheduleActions(ActionGroupId, baseContext); + } + + // this is the state that we can access in the next execution + return { + latestTimestamp: timestamp, + }; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index de9c583ef885e..e8cac552edcd7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -22,12 +22,26 @@ export interface EsQueryAlertState extends AlertTypeState { export const EsQueryAlertParamsSchemaProperties = { index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), timeField: schema.string({ minLength: 1 }), - esQuery: schema.string({ minLength: 1 }), - size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), + + esQuery: schema.conditional( + schema.siblingRef('searchType'), + schema.literal(''), + schema.string({ minLength: 1 }), + schema.never() + ), + size: schema.conditional( + schema.siblingRef('searchType'), + schema.literal('esQuery'), + schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), + schema.never() + ), + timeWindowSize: schema.number({ min: 1 }), timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), thresholdComparator: schema.string({ validate: validateComparator }), + searchType: schema.oneOf([schema.literal('esQuery'), schema.literal('searchSource')]), + searchConfiguration: schema.object({}, { unknowns: 'allow' }), }; export const EsQueryAlertParamsSchema = schema.object(EsQueryAlertParamsSchemaProperties, { @@ -38,7 +52,7 @@ const betweenComparators = new Set(['between', 'notBetween']); // using direct type not allowed, circular reference, so body is typed to any function validateParams(anyParams: unknown): string | undefined { - const { esQuery, thresholdComparator, threshold }: EsQueryAlertParams = + const { esQuery, thresholdComparator, threshold, searchType }: EsQueryAlertParams = anyParams as EsQueryAlertParams; if (betweenComparators.has(thresholdComparator) && threshold.length === 1) { @@ -51,6 +65,10 @@ function validateParams(anyParams: unknown): string | undefined { }); } + if (searchType === 'searchSource') { + return; + } + try { const parsedQuery = JSON.parse(esQuery); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts index ffe0388e64216..99f9eb1795688 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/index.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { Logger } from 'src/core/server'; +import { CoreSetup, Logger } from 'src/core/server'; import { AlertingSetup } from '../../types'; import { getAlertType } from './alert_type'; interface RegisterParams { logger: Logger; alerting: AlertingSetup; + core: CoreSetup; } export function register(params: RegisterParams) { - const { logger, alerting } = params; - alerting.registerType(getAlertType(logger)); + const { logger, alerting, core } = params; + alerting.registerType(getAlertType(logger, core)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index.ts index 219ccad2ece63..8ad10298f351b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger } from 'src/core/server'; +import { CoreSetup, Logger } from 'src/core/server'; import { AlertingSetup, StackAlertsStartDeps } from '../types'; import { register as registerIndexThreshold } from './index_threshold'; import { register as registerGeoContainment } from './geo_containment'; @@ -14,6 +14,7 @@ interface RegisterAlertTypesParams { logger: Logger; data: Promise; alerting: AlertingSetup; + core: CoreSetup; } export function registerBuiltInAlertTypes(params: RegisterAlertTypesParams) { diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index 1a671466b69b3..ac8f60de88b6f 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -29,6 +29,7 @@ export class AlertingBuiltinsPlugin .getStartServices() .then(async ([, { triggersActionsUi }]) => triggersActionsUi.data), alerting, + core, }); } From 6954568c929d35fc3245f11d3b88b47661817cab Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 9 Feb 2022 14:24:22 +0300 Subject: [PATCH 02/67] [Discover] change filters in alert expression --- src/plugins/data/public/index.ts | 7 ++- .../data/public/ui/filter_bar/filter_item.tsx | 39 ++++++++----- .../ui/filter_bar/filter_view/index.tsx | 56 +++++++++++-------- .../components/read_only_filter_items.tsx | 56 +++++++++++++++++++ .../es_query/{constans.ts => constants.ts} | 0 .../es_query/es_query_expression.tsx | 2 +- .../alert_types/es_query/expression.tsx | 4 +- .../search_source_threshold_expression.scss | 6 ++ ...=> search_source_threshold_expression.tsx} | 51 +++++------------ 9 files changed, 144 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/components/read_only_filter_items.tsx rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{constans.ts => constants.ts} (100%) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.scss rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{index_threshold_expression.tsx => search_source_threshold_expression.tsx} (82%) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index dda5c8890697a..ff74a15307234 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -20,7 +20,12 @@ export * from './deprecated'; export { getEsQueryConfig } from '../common'; export { FilterLabel, FilterItem, FilterBar } from './ui'; -export { getDisplayValueFromFilter, generateFilters, extractTimeRange } from './query'; +export { + getDisplayValueFromFilter, + generateFilters, + extractTimeRange, + getIndexPatternFromFilter, +} from './query'; /** * Exporters (CSV) diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 9e535513aa014..e38ef9a835b3c 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EuiContextMenu, EuiPopover } from '@elastic/eui'; +import { CommonProps, EuiContextMenu, EuiPopover, EuiPopoverProps } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n-react'; import { Filter, @@ -16,7 +16,7 @@ import { toggleFilterDisabled, } from '@kbn/es-query'; import classNames from 'classnames'; -import React, { MouseEvent, useState, useEffect } from 'react'; +import React, { MouseEvent, useState, useEffect, HTMLAttributes } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; @@ -37,6 +37,7 @@ export interface FilterItemProps { uiSettings: IUiSettingsClient; hiddenPanelOptions?: PanelOptions[]; timeRangeForSuggestionsOverride?: boolean; + readonly?: boolean; } interface LabelOptions { @@ -359,22 +360,32 @@ export function FilterItem(props: FilterItemProps) { iconOnClick={() => props.onRemove()} onClick={handleBadgeClick} data-test-subj={getDataTestSubj(valueLabelConfig)} + readonly={props.readonly} /> ); + const popoverProps: CommonProps & HTMLAttributes & EuiPopoverProps = { + id: `popoverFor_filter${id}`, + className: `globalFilterItem__popover`, + anchorClassName: `globalFilterItem__popoverAnchor`, + isOpen: isPopoverOpen, + closePopover: () => { + setIsPopoverOpen(false); + }, + button: badge, + panelPaddingSize: 'none', + }; + + if (props.readonly) { + return ( + + {badge} + + ); + } + return ( - { - setIsPopoverOpen(false); - }} - button={badge} - anchorPosition="downLeft" - panelPaddingSize="none" - > + ); diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index d551af87c7279..17e5bfa977a29 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EuiBadge, useInnerText } from '@elastic/eui'; +import { EuiBadge, EuiBadgeProps, useInnerText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; import { Filter, isFilterPinned } from '@kbn/es-query'; @@ -18,6 +18,7 @@ interface Props { valueLabel: string; filterLabelStatus: FilterLabelStatus; errorMessage?: string; + readonly?: boolean; [propName: string]: any; } @@ -28,6 +29,7 @@ export const FilterView: FC = ({ valueLabel, errorMessage, filterLabelStatus, + readonly, ...rest }: Props) => { const [ref, innerText] = useInnerText(); @@ -50,28 +52,38 @@ export const FilterView: FC = ({ })} ${title}`; } + const badgeProps: EuiBadgeProps = readonly + ? { + title, + onClick, + onClickAriaLabel: i18n.translate('data.filter.filterBar.filterItemReadOnlyBadgeAriaLabel', { + defaultMessage: 'Filter entry', + }), + color: 'hollow', + } + : { + title, + color: 'hollow', + iconType: 'cross', + iconSide: 'right', + closeButtonProps: { + // Removing tab focus on close button because the same option can be obtained through the context menu + // Also, we may want to add a `DEL` keyboard press functionality + tabIndex: -1, + }, + iconOnClick, + iconOnClickAriaLabel: i18n.translate('data.filter.filterBar.filterItemBadgeIconAriaLabel', { + defaultMessage: 'Delete {filter}', + values: { filter: innerText }, + }), + onClick, + onClickAriaLabel: i18n.translate('data.filter.filterBar.filterItemBadgeAriaLabel', { + defaultMessage: 'Filter actions', + }), + }; + return ( - + { + const { uiSettings } = useKibana().services; + + const filterList = filters.map((filter, index) => { + const filterValue = getDisplayValueFromFilter(filter, indexPatterns); + return ( + + {}} + onRemove={() => {}} + indexPatterns={indexPatterns} + uiSettings={uiSettings!} + readonly + /> + + ); + }); + + return ( + + {filterList} + + ); +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/constans.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/constants.ts similarity index 100% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/constans.ts rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/constants.ts diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx index 7e6fccef14825..7caaad3a192aa 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx @@ -30,7 +30,7 @@ import { parseDuration } from '../../../../alerting/common'; import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; import { EsQueryAlertParams } from './types'; import { IndexSelectPopover } from '../components/index_select_popover'; -import { DEFAULT_VALUES } from './constans'; +import { DEFAULT_VALUES } from './constants'; function totalHitsToNumber(total: estypes.SearchHitsMetadata['total']): number { return typeof total === 'number' ? total : total?.value ?? 0; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index 5820a5661612b..5dec5838ba559 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -13,7 +13,7 @@ import 'brace/theme/github'; import { EuiSpacer, EuiCallOut } from '@elastic/eui'; import { RuleTypeParamsExpressionProps } from '../../../../triggers_actions_ui/public'; import { EsQueryAlertParams } from './types'; -import { IndexThresholdParameters } from './index_threshold_expression'; +import { SearchSourceThresholdExpression } from './search_source_threshold_expression'; import { EsQueryExpression } from './es_query_expression'; const expressionFieldsWithValidation = [ @@ -63,7 +63,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< )} {isIndexThreshold ? ( - + ) : ( )} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.scss b/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.scss new file mode 100644 index 0000000000000..0742e1cab7a27 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.scss @@ -0,0 +1,6 @@ +.searchSourceAlertFilters { + .euiExpression__value { + width: 80%; + } +} + \ No newline at end of file diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index_threshold_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx similarity index 82% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/index_threshold_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx index bf8a44ed7db95..7bebae9d27d57 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index_threshold_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx @@ -6,20 +6,21 @@ */ import React, { Fragment, useCallback, useEffect, useState } from 'react'; +import './search_source_threshold_expression.scss'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, EuiTitle, EuiExpression, EuiPopover, EuiText } from '@elastic/eui'; import { Filter, ISearchSource } from '../../../../../../src/plugins/data/common'; import { QueryStringInput } from '../../../../../../src/plugins/data/public'; -import { FilterBar } from '../../../../../../src/plugins/data/public'; import { EsQueryAlertParams } from './types'; import { ForLastExpression, RuleTypeParamsExpressionProps, ThresholdExpression, } from '../../../../triggers_actions_ui/public'; -import { DEFAULT_VALUES } from './constans'; +import { DEFAULT_VALUES } from './constants'; +import { ReadOnlyFilterItems } from '../components/read_only_filter_items'; -export const IndexThresholdParameters = ({ +export const SearchSourceThresholdExpression = ({ ruleParams, setRuleParams, setRuleProperty, @@ -60,7 +61,6 @@ export const IndexThresholdParameters = ({ }, []); const [usedSearchSource, setUsedSearchSource] = useState(); - const editable = false; const { searchConfiguration } = ruleParams; @@ -84,7 +84,9 @@ export const IndexThresholdParameters = ({ return null; } - const filterArr = usedSearchSource.getField('filter') as Filter[]; + const filters = (usedSearchSource.getField('filter') as Filter[]).filter( + ({ meta }) => !meta.disabled + ); return ( @@ -110,11 +112,6 @@ export const IndexThresholdParameters = ({ value={usedSearchSource.getField('query')!.query} isActive={true} display="columns" - onClick={() => { - if (editable) { - setShowQueryBar(!showQueryBar); - } - }} /> } display="block" @@ -134,43 +131,23 @@ export const IndexThresholdParameters = ({ { - // currently it's just displaying the filter key - // but of course this needs to be improved - return filter.meta.key; - }) - .join(', ') - : '' + } isActive={true} display="columns" - onClick={() => { - if (editable) { - setShowFilter(!showFilter); - } - }} /> } display="block" isOpen={showFilter} closePopover={() => setShowFilter(false)} - > - { - usedSearchSource.setField('filter', filters); - setUsedSearchSource(usedSearchSource.createCopy()); - }} - /> - + /> Date: Wed, 9 Feb 2022 16:47:56 +0300 Subject: [PATCH 03/67] [Discover] fix cursor issue --- src/plugins/data/public/ui/filter_bar/filter_view/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 17e5bfa977a29..8066a56021247 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -55,11 +55,12 @@ export const FilterView: FC = ({ const badgeProps: EuiBadgeProps = readonly ? { title, + color: 'hollow', onClick, onClickAriaLabel: i18n.translate('data.filter.filterBar.filterItemReadOnlyBadgeAriaLabel', { defaultMessage: 'Filter entry', }), - color: 'hollow', + iconOnClick, } : { title, From 98da07a84de841a4a44d71309b6a50d5406672bd Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 9 Feb 2022 18:02:24 +0300 Subject: [PATCH 04/67] [Discover] add loading --- .../read_only_filter_items.tsx | 0 .../search_source_threshold_expression.tsx | 50 +++++++++++-------- 2 files changed, 30 insertions(+), 20 deletions(-) rename x-pack/plugins/stack_alerts/public/alert_types/{components => es_query}/read_only_filter_items.tsx (100%) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/read_only_filter_items.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/read_only_filter_items.tsx similarity index 100% rename from x-pack/plugins/stack_alerts/public/alert_types/components/read_only_filter_items.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/read_only_filter_items.tsx diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx index 7bebae9d27d57..26bd35d4ac6ac 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx @@ -8,7 +8,15 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'; import './search_source_threshold_expression.scss'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiSpacer, EuiTitle, EuiExpression, EuiPopover, EuiText } from '@elastic/eui'; +import { + EuiSpacer, + EuiTitle, + EuiExpression, + EuiPopover, + EuiText, + EuiLoadingSpinner, + EuiEmptyPrompt, +} from '@elastic/eui'; import { Filter, ISearchSource } from '../../../../../../src/plugins/data/common'; import { QueryStringInput } from '../../../../../../src/plugins/data/public'; import { EsQueryAlertParams } from './types'; @@ -18,7 +26,7 @@ import { ThresholdExpression, } from '../../../../triggers_actions_ui/public'; import { DEFAULT_VALUES } from './constants'; -import { ReadOnlyFilterItems } from '../components/read_only_filter_items'; +import { ReadOnlyFilterItems } from './read_only_filter_items'; export const SearchSourceThresholdExpression = ({ ruleParams, @@ -80,13 +88,27 @@ export const SearchSourceThresholdExpression = ({ }, [data.search.searchSource, searchConfiguration]); if (!usedSearchSource) { - // there should be a loading indicator - return null; + return ( + } + body={ + + + + } + /> + ); } + const dataView = usedSearchSource.getField('index')!; + const query = usedSearchSource.getField('query')!; const filters = (usedSearchSource.getField('filter') as Filter[]).filter( ({ meta }) => !meta.disabled ); + const indexPatterns = [dataView]; return ( @@ -101,7 +123,7 @@ export const SearchSourceThresholdExpression = ({ @@ -109,7 +131,7 @@ export const SearchSourceThresholdExpression = ({ button={ @@ -118,14 +140,7 @@ export const SearchSourceThresholdExpression = ({ isOpen={showQueryBar} closePopover={() => setShowQueryBar(false)} > - { - usedSearchSource.setField('query', query); - setUsedSearchSource(usedSearchSource.createCopy()); - }} - /> + - } + value={} isActive={true} display="columns" /> From 369144d1e5005d371a5df83c4d00451201ea90cb Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Sun, 13 Feb 2022 18:53:48 +0300 Subject: [PATCH 05/67] [Discover] separate validation params --- .../top_nav/open_alerts_popover.tsx | 4 +- .../{ => expression}/es_query_expression.tsx | 48 ++- .../{ => expression}/expression.test.tsx | 19 +- .../es_query/{ => expression}/expression.tsx | 29 +- .../alert_types/es_query/expression/index.ts | 11 + .../read_only_filter_items.tsx | 9 +- .../search_source_expression.scss} | 4 + .../search_source_expression.tsx} | 131 ++++---- .../public/alert_types/es_query/types.ts | 24 +- .../public/alert_types/es_query/util.ts | 14 + .../alert_types/es_query/validation.test.ts | 38 ++- .../public/alert_types/es_query/validation.ts | 77 +++-- .../alert_types/es_query/action_context.ts | 4 +- .../server/alert_types/es_query/alert_type.ts | 306 +----------------- .../alert_types/es_query/alert_type_params.ts | 36 ++- .../server/alert_types/es_query/constants.ts | 10 + .../es_query/executor/es_query_executor.ts | 208 ++++++++++++ .../alert_types/es_query/executor/index.ts | 11 + .../executor/search_source_executor.ts | 103 ++++++ .../server/alert_types/es_query/types.ts | 30 ++ x-pack/plugins/stack_alerts/server/feature.ts | 2 +- x-pack/plugins/stack_alerts/server/types.ts | 1 + 22 files changed, 641 insertions(+), 478 deletions(-) rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{ => expression}/es_query_expression.tsx (88%) rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{ => expression}/expression.test.tsx (90%) rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{ => expression}/expression.tsx (74%) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/index.ts rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{ => expression}/read_only_filter_items.tsx (84%) rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{search_source_threshold_expression.scss => expression/search_source_expression.scss} (58%) rename x-pack/plugins/stack_alerts/public/alert_types/es_query/{search_source_threshold_expression.tsx => expression/search_source_expression.tsx} (66%) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/constants.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/es_query_executor.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/index.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/search_source_executor.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index f3a20fb42278f..3729be612b555 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -52,12 +52,10 @@ export function AlertsPopover(props: AlertsPopoverProps) { }); return { - index: [dataView?.id], - timeField: dataView?.timeFieldName, searchType: 'searchSource', searchConfiguration: nextSearchSource.getSerializedFields(), }; - }, [searchSource, services, dataView]); + }, [searchSource, services]); const SearchThresholdAlertFlyout = useMemo(() => { if (!alertFlyoutVisible) { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx similarity index 88% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx index 7caaad3a192aa..20489ef16a8f4 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/es_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx @@ -16,21 +16,21 @@ import { EuiButtonEmpty, EuiSpacer, EuiFormRow, EuiTitle, EuiLink, EuiText } fro import { DocLinksStart, HttpSetup } from 'kibana/public'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { XJson, EuiCodeEditor } from '../../../../../../src/plugins/es_ui_shared/public'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { XJson, EuiCodeEditor } from '../../../../../../../src/plugins/es_ui_shared/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { getFields, ValueExpression, RuleTypeParamsExpressionProps, ForLastExpression, ThresholdExpression, -} from '../../../../triggers_actions_ui/public'; -import { validateExpression } from './validation'; -import { parseDuration } from '../../../../alerting/common'; -import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; -import { EsQueryAlertParams } from './types'; -import { IndexSelectPopover } from '../components/index_select_popover'; -import { DEFAULT_VALUES } from './constants'; +} from '../../../../../triggers_actions_ui/public'; +import { validateExpression } from '../validation'; +import { parseDuration } from '../../../../../alerting/common'; +import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query'; +import { EsQueryAlertParams, SearchType } from '../types'; +import { IndexSelectPopover } from '../../components/index_select_popover'; +import { DEFAULT_VALUES } from '../constants'; function totalHitsToNumber(total: estypes.SearchHitsMetadata['total']): number { return typeof total === 'number' ? total : total?.value ?? 0; @@ -50,7 +50,7 @@ export const EsQueryExpression = ({ setRuleProperty, errors, data, -}: RuleTypeParamsExpressionProps) => { +}: RuleTypeParamsExpressionProps>) => { const { index, timeField, @@ -62,22 +62,18 @@ export const EsQueryExpression = ({ timeWindowUnit, } = ruleParams; - const getDefaultParams = () => { - return { - ...ruleParams, - timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, - timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, - threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, - thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, - size: size ?? DEFAULT_VALUES.SIZE, - searchType: 'esQuery', - esQuery: esQuery ?? DEFAULT_VALUES.QUERY, - }; - }; - - const [currentAlertParams, setCurrentAlertParams] = useState( - getDefaultParams() - ); + const [currentAlertParams, setCurrentAlertParams] = useState< + EsQueryAlertParams + >({ + ...ruleParams, + timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, + thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, + size: size ?? DEFAULT_VALUES.SIZE, + searchType: SearchType.esQuery, + esQuery: esQuery ?? DEFAULT_VALUES.QUERY, + }); const setParam = useCallback( (paramField: string, paramValue: unknown) => { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.test.tsx similarity index 90% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.test.tsx index 7ecdcd6dbce38..03d9b0fe4e90d 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.test.tsx @@ -10,7 +10,7 @@ import 'brace'; import { of } from 'rxjs'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import EsQueryAlertTypeExpression from './expression'; +import EsQueryAlertTypeExpression from '.'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { @@ -18,11 +18,11 @@ import { IKibanaSearchResponse, ISearchStart, } from 'src/plugins/data/public'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { EsQueryAlertParams } from './types'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { EsQueryAlertParams, SearchType } from '../types'; -jest.mock('../../../../../../src/plugins/kibana_react/public'); -jest.mock('../../../../../../src/plugins/es_ui_shared/public', () => ({ +jest.mock('../../../../../../../src/plugins/kibana_react/public'); +jest.mock('../../../../../../../src/plugins/es_ui_shared/public', () => ({ XJson: { useXJsonMode: jest.fn().mockReturnValue({ convertToJson: jest.fn(), @@ -42,8 +42,8 @@ jest.mock('../../../../../../src/plugins/es_ui_shared/public', () => ({ /> ), })); -jest.mock('../../../../triggers_actions_ui/public', () => { - const original = jest.requireActual('../../../../triggers_actions_ui/public'); +jest.mock('../../../../../triggers_actions_ui/public', () => { + const original = jest.requireActual('../../../../../triggers_actions_ui/public'); return { ...original, getIndexPatterns: () => { @@ -117,7 +117,7 @@ describe('EsQueryAlertTypeExpression', () => { }); }); - function getAlertParams(overrides = {}) { + function getAlertParams(overrides = {}): EsQueryAlertParams { return { index: ['test-index'], timeField: '@timestamp', @@ -127,10 +127,11 @@ describe('EsQueryAlertTypeExpression', () => { threshold: [0], timeWindowSize: 15, timeWindowUnit: 's', + searchType: SearchType.esQuery, ...overrides, }; } - async function setup(alertParams: EsQueryAlertParams) { + async function setup(alertParams: EsQueryAlertParams) { const errors = { index: [], esQuery: [], diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx similarity index 74% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx index 5dec5838ba559..fab6d935837dc 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx @@ -11,31 +11,29 @@ import { i18n } from '@kbn/i18n'; import 'brace/theme/github'; import { EuiSpacer, EuiCallOut } from '@elastic/eui'; -import { RuleTypeParamsExpressionProps } from '../../../../triggers_actions_ui/public'; -import { EsQueryAlertParams } from './types'; -import { SearchSourceThresholdExpression } from './search_source_threshold_expression'; +import { RuleTypeParamsExpressionProps } from '../../../../../triggers_actions_ui/public'; +import { EsQueryAlertParams } from '../types'; +import { SearchSourceThresholdExpression } from './search_source_expression'; import { EsQueryExpression } from './es_query_expression'; +import { isSearchSourceAlert } from '../util'; const expressionFieldsWithValidation = [ 'index', - 'esQuery', 'size', 'timeField', 'threshold0', 'threshold1', 'timeWindowSize', + 'searchType', + 'esQuery', + 'searchConfiguration', ]; export const EsQueryAlertTypeExpression: React.FunctionComponent< RuleTypeParamsExpressionProps > = (props) => { - const { - ruleParams: { searchType }, - ruleParams, - errors, - } = props; - - const isIndexThreshold = searchType === 'searchSource'; + const { ruleParams, errors } = props; + const isSearchSource = isSearchSourceAlert(ruleParams); const hasExpressionErrors = !!Object.keys(errors).find((errorKey) => { return ( @@ -62,14 +60,11 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< )} - {isIndexThreshold ? ( - + {isSearchSource ? ( + ) : ( - + )} ); }; - -// eslint-disable-next-line import/no-default-export -export { EsQueryAlertTypeExpression as default }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/index.ts new file mode 100644 index 0000000000000..ee3eb83299748 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { EsQueryAlertTypeExpression } from './expression'; + +// eslint-disable-next-line import/no-default-export +export default EsQueryAlertTypeExpression; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/read_only_filter_items.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx similarity index 84% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/read_only_filter_items.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx index 610b01971c843..f62d3b9871eb3 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/read_only_filter_items.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx @@ -9,9 +9,12 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { injectI18n } from '@kbn/i18n-react'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { FilterItem, getDisplayValueFromFilter } from '../../../../../../src/plugins/data/public'; -import { Filter, IIndexPattern } from '../../../../../../src/plugins/data/common'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { + FilterItem, + getDisplayValueFromFilter, +} from '../../../../../../../src/plugins/data/public'; +import { Filter, IIndexPattern } from '../../../../../../../src/plugins/data/common'; const FilterItemComponent = injectI18n(FilterItem); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.scss b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss similarity index 58% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.scss rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss index 0742e1cab7a27..d34383a2569dc 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.scss +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss @@ -3,4 +3,8 @@ width: 80%; } } + +.dscExpressionParam.euiExpression { + margin-left: 0; +} \ No newline at end of file diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx similarity index 66% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 26bd35d4ac6ac..2d924518f7c62 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/search_source_threshold_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -6,26 +6,26 @@ */ import React, { Fragment, useCallback, useEffect, useState } from 'react'; -import './search_source_threshold_expression.scss'; +import './search_source_expression.scss'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, EuiTitle, EuiExpression, - EuiPopover, EuiText, EuiLoadingSpinner, EuiEmptyPrompt, } from '@elastic/eui'; -import { Filter, ISearchSource } from '../../../../../../src/plugins/data/common'; -import { QueryStringInput } from '../../../../../../src/plugins/data/public'; -import { EsQueryAlertParams } from './types'; +import { i18n } from '@kbn/i18n'; +import { Filter, ISearchSource } from '../../../../../../../src/plugins/data/common'; +import { EsQueryAlertParams, SearchType } from '../types'; import { ForLastExpression, RuleTypeParamsExpressionProps, ThresholdExpression, -} from '../../../../triggers_actions_ui/public'; -import { DEFAULT_VALUES } from './constants'; + ValueExpression, +} from '../../../../../triggers_actions_ui/public'; +import { DEFAULT_VALUES } from '../constants'; import { ReadOnlyFilterItems } from './read_only_filter_items'; export const SearchSourceThresholdExpression = ({ @@ -34,23 +34,28 @@ export const SearchSourceThresholdExpression = ({ setRuleProperty, data, errors, -}: RuleTypeParamsExpressionProps) => { - const { thresholdComparator, threshold, timeWindowSize, timeWindowUnit } = ruleParams; - - const getDefaultParams = () => { - const defaults = { - timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, - timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, - threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, - thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, - }; +}: RuleTypeParamsExpressionProps>) => { + const { + searchConfiguration, + thresholdComparator, + threshold, + timeWindowSize, + timeWindowUnit, + size, + } = ruleParams; + const [usedSearchSource, setUsedSearchSource] = useState(); - return { ...ruleParams, ...defaults, searchType: 'searchSource' }; - }; - - const [currentAlertParams, setCurrentAlertParams] = useState( - getDefaultParams() - ); + const [currentAlertParams, setCurrentAlertParams] = useState< + EsQueryAlertParams + >({ + searchConfiguration, + searchType: SearchType.searchSource, + timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, + timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, + threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, + thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, + size: size ?? DEFAULT_VALUES.SIZE, + }); const setParam = useCallback( (paramField: string, paramValue: unknown) => { @@ -68,15 +73,6 @@ export const SearchSourceThresholdExpression = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [usedSearchSource, setUsedSearchSource] = useState(); - - const { searchConfiguration } = ruleParams; - - // Note that this PR contains a limited way to edit query and filter - // But it's out of scope for the MVP - const [showQueryBar, setShowQueryBar] = useState(false); - const [showFilter, setShowFilter] = useState(false); - useEffect(() => { async function initSearchSource() { const loadedSearchSource = await data.search.searchSource.create(searchConfiguration); @@ -109,7 +105,6 @@ export const SearchSourceThresholdExpression = ({ ({ meta }) => !meta.disabled ); const indexPatterns = [dataView]; - return ( @@ -122,41 +117,26 @@ export const SearchSourceThresholdExpression = ({ - - } - display="block" - isOpen={showQueryBar} - closePopover={() => setShowQueryBar(false)} - > - - - - } - isActive={true} - display="columns" - /> - } - display="block" - isOpen={showFilter} - closePopover={() => setShowFilter(false)} + + } + isActive={true} + display="columns" /> + + + +
+ +
+
+ + { + setParam('size', updatedValue); + }} + /> +
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts index 2add72ac79cf6..702dbfa5d2a3a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts @@ -14,15 +14,29 @@ export interface Comparator { requiredValues: number; } -export interface EsQueryAlertParams extends AlertTypeParams { - index: string[]; - timeField?: string; - esQuery: string; +export enum SearchType { + esQuery = 'esQuery', + searchSource = 'searchSource', +} + +export interface CommonAlertParams extends AlertTypeParams { size: number; thresholdComparator?: string; threshold: number[]; timeWindowSize: number; timeWindowUnit: string; - searchType: string; + searchType: T; +} + +export type EsQueryAlertParams = T extends SearchType.esQuery + ? CommonAlertParams & OnlyEsQueryAlertParams + : CommonAlertParams & OnlySearchSourceAlertParams; + +export interface OnlyEsQueryAlertParams { + esQuery: string; + index: string[]; + timeField: string; +} +export interface OnlySearchSourceAlertParams { searchConfiguration: SerializedSearchSourceFields; } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts new file mode 100644 index 0000000000000..5b70da7cb3e80 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/util.ts @@ -0,0 +1,14 @@ +/* + * 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 { EsQueryAlertParams, SearchType } from './types'; + +export const isSearchSourceAlert = ( + ruleParams: EsQueryAlertParams +): ruleParams is EsQueryAlertParams => { + return ruleParams.searchType === 'searchSource'; +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts index 52278b4576557..a3f5e0fce1942 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts @@ -5,64 +5,72 @@ * 2.0. */ -import { EsQueryAlertParams } from './types'; +import { EsQueryAlertParams, SearchType } from './types'; import { validateExpression } from './validation'; describe('expression params validation', () => { test('if index property is invalid should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: [], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.index.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.index[0]).toBe('Index is required.'); }); test('if timeField property is not defined should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.timeField.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.timeField[0]).toBe('Time field is required.'); }); test('if esQuery property is invalid JSON should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.esQuery[0]).toBe('Query must be valid JSON.'); }); test('if esQuery property is invalid should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"aggs\":{\n \"match_all\" : {}\n }\n}`, size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.esQuery[0]).toBe(`Query field is required.`); }); test('if threshold0 property is not set should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, @@ -70,13 +78,15 @@ describe('expression params validation', () => { timeWindowSize: 1, timeWindowUnit: 's', thresholdComparator: '<', + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.threshold0.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.threshold0[0]).toBe('Threshold 0 is required.'); }); test('if threshold1 property is needed by thresholdComparator but not set should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, @@ -84,13 +94,15 @@ describe('expression params validation', () => { timeWindowSize: 1, timeWindowUnit: 's', thresholdComparator: 'between', + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.threshold1[0]).toBe('Threshold 1 is required.'); }); test('if threshold0 property greater than threshold1 property should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, size: 100, @@ -98,6 +110,8 @@ describe('expression params validation', () => { timeWindowSize: 1, timeWindowUnit: 's', thresholdComparator: 'between', + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.threshold1[0]).toBe( @@ -106,13 +120,15 @@ describe('expression params validation', () => { }); test('if size property is < 0 should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, size: -1, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.size[0]).toBe( @@ -121,13 +137,15 @@ describe('expression params validation', () => { }); test('if size property is > 10000 should return proper error message', () => { - const initialParams: EsQueryAlertParams = { + const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, size: 25000, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], + timeField: '', + searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.size[0]).toBe( diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts index c2eb5a4536b04..1e23aba703527 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts @@ -8,18 +8,10 @@ import { i18n } from '@kbn/i18n'; import { EsQueryAlertParams } from './types'; import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public'; +import { isSearchSourceAlert } from './util'; export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => { - const { - index, - timeField, - esQuery, - size, - threshold, - timeWindowSize, - thresholdComparator, - searchType, - } = alertParams; + const { size, threshold, timeWindowSize, thresholdComparator } = alertParams; const validationResult = { errors: {} }; const errors = { index: new Array(), @@ -30,6 +22,7 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR threshold1: new Array(), thresholdComparator: new Array(), timeWindowSize: new Array(), + searchConfiguration: new Array(), }; validationResult.errors = errors; if (!threshold || threshold.length === 0 || threshold[0] === undefined) { @@ -66,23 +59,43 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR }) ); } - if (!timeField) { - errors.timeField.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredTimeFieldText', { - defaultMessage: 'Time field is required.', + + if (!size) { + errors.size.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', { + defaultMessage: 'Size is required.', + }) + ); + } + if ((size && size < 0) || size > 10000) { + errors.size.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.invalidSizeRangeText', { + defaultMessage: 'Size must be between 0 and {max, number}.', + values: { max: 10000 }, }) ); } /** - * Skip esQuery, size, timeField, index checks if it is .index-threshold, - * since it should contain searchConfiguration + * Skip esQuery and index params check if it is search source alert, + * since it should contain searchConfiguration instead of esQuery and index. */ - if (searchType === 'searchSource') { + const isSearchSource = isSearchSourceAlert(alertParams); + if (isSearchSource) { + if (!alertParams.searchConfiguration) { + errors.index.push( + i18n.translate( + 'xpack.stackAlerts.esQuery.ui.validation.error.requiredSearchConfiguration', + { + defaultMessage: 'Search source configuration is required.', + } + ) + ); + } return validationResult; } - if (!index || index.length === 0) { + if (!alertParams.index || alertParams.index.length === 0) { errors.index.push( i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredIndexText', { defaultMessage: 'Index is required.', @@ -90,7 +103,15 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR ); } - if (!esQuery) { + if (!alertParams.timeField) { + errors.timeField.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredTimeFieldText', { + defaultMessage: 'Time field is required.', + }) + ); + } + + if (!alertParams.esQuery) { errors.esQuery.push( i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredQueryText', { defaultMessage: 'Elasticsearch query is required.', @@ -98,7 +119,7 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR ); } else { try { - const parsedQuery = JSON.parse(esQuery); + const parsedQuery = JSON.parse(alertParams.esQuery); if (!parsedQuery.query) { errors.esQuery.push( i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredEsQueryText', { @@ -114,20 +135,6 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR ); } } - if (!size) { - errors.size.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', { - defaultMessage: 'Size is required.', - }) - ); - } - if ((size && size < 0) || size > 10000) { - errors.size.push( - i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.invalidSizeRangeText', { - defaultMessage: 'Size must be between 0 and {max, number}.', - values: { max: 10000 }, - }) - ); - } + return validationResult; }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index 74c3e53e95c00..2957f21f662f7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { AlertExecutorOptions, AlertInstanceContext } from '../../../../alerting/server'; -import { EsQueryAlertParams } from './alert_type_params'; +import { OnlyEsQueryAlertParams } from './types'; // alert type context provided to actions @@ -35,7 +35,7 @@ export interface EsQueryAlertActionContext extends AlertInstanceContext { export function addMessages( alertInfo: AlertInfo, baseContext: EsQueryAlertActionContext, - params: EsQueryAlertParams + params: OnlyEsQueryAlertParams ): ActionContext { const title = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle', { defaultMessage: `alert '{name}' matched query`, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 61412e095828f..0ff4af8979871 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -6,34 +6,18 @@ */ import { i18n } from '@kbn/i18n'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { sha256 } from 'js-sha256'; import { CoreSetup, Logger } from 'src/core/server'; -import { RuleType, AlertExecutorOptions } from '../../types'; -import { ActionContext, EsQueryAlertActionContext, addMessages } from './action_context'; +import { RuleType } from '../../types'; +import { ActionContext } from './action_context'; import { EsQueryAlertParams, EsQueryAlertParamsSchema, EsQueryAlertState, } from './alert_type_params'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; -import { ComparatorFns, getHumanReadableComparator } from '../lib'; -import { parseDuration } from '../../../../alerting/server'; -import { buildSortedEventsQuery } from '../../../common/build_sorted_events_query'; -import { getTime } from '../../../../../../src/plugins/data/common'; - -export const ES_QUERY_ID = '.es-query'; - -export const ActionGroupId = 'query matched'; -export const ConditionMetAlertInstanceId = 'query matched'; - -type ExecutorOptions = AlertExecutorOptions< - EsQueryAlertParams, - EsQueryAlertState, - {}, - ActionContext, - typeof ActionGroupId ->; +import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; +import { ActionGroupId, ES_QUERY_ID } from './constants'; +import { esQueryExecutor, searchSourceExecutor } from './executor'; export function getAlertType( logger: Logger, @@ -163,279 +147,19 @@ export function getAlertType( producer: STACK_ALERTS_FEATURE_ID, }; - async function executor(options: ExecutorOptions) { - if (options.params.searchType === 'searchSource') { - return await indexThresholdExpressionExecutor(logger, core, options); + async function executor(options: ExecutorOptions) { + if (isEsQueryAlert(options)) { + return await esQueryExecutor(logger, options as ExecutorOptions); } else { - return await esQueryExpressionExecutor(logger, options); + return await searchSourceExecutor( + logger, + core, + options as ExecutorOptions + ); } } } -function getValidTimefieldSort(sortValues: Array = []): undefined | string { - for (const sortValue of sortValues) { - const sortDate = tryToParseAsDate(sortValue); - if (sortDate) { - return sortDate; - } - } -} -function tryToParseAsDate(sortValue?: string | number | null): undefined | string { - const sortDate = typeof sortValue === 'string' ? Date.parse(sortValue) : sortValue; - if (sortDate && !isNaN(sortDate)) { - return new Date(sortDate).toISOString(); - } -} - -function getInvalidComparatorError(comparator: string) { - return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator, - }, - }); -} - -function getInvalidWindowSizeError(windowValue: string) { - return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', { - defaultMessage: 'invalid format for windowSize: "{windowValue}"', - values: { - windowValue, - }, - }); -} - -function getInvalidQueryError(query: string) { - return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', { - defaultMessage: 'invalid query specified: "{query}" - query must be JSON', - values: { - query, - }, - }); -} - -function getSearchParams(queryParams: EsQueryAlertParams) { - const date = Date.now(); - const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; - - let parsedQuery; - try { - parsedQuery = JSON.parse(esQuery); - } catch (err) { - throw new Error(getInvalidQueryError(esQuery)); - } - - if (parsedQuery && !parsedQuery.query) { - throw new Error(getInvalidQueryError(esQuery)); - } - - const window = `${timeWindowSize}${timeWindowUnit}`; - let timeWindow: number; - try { - timeWindow = parseDuration(window); - } catch (err) { - throw new Error(getInvalidWindowSizeError(window)); - } - - const dateStart = new Date(date - timeWindow).toISOString(); - const dateEnd = new Date(date).toISOString(); - - return { parsedQuery, dateStart, dateEnd }; -} - -async function esQueryExpressionExecutor(logger: Logger, options: ExecutorOptions) { - const { alertId, name, services, params, state } = options; - const { alertFactory, search } = services; - const previousTimestamp = state.latestTimestamp; - - const abortableEsClient = search.asCurrentUser; - const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); - - const compareFn = ComparatorFns.get(params.thresholdComparator); - if (compareFn == null) { - throw new Error(getInvalidComparatorError(params.thresholdComparator)); - } - - // During each alert execution, we run the configured query, get a hit count - // (hits.total) and retrieve up to params.size hits. We - // evaluate the threshold condition using the value of hits.total. If the threshold - // condition is met, the hits are counted toward the query match and we update - // the alert state with the timestamp of the latest hit. In the next execution - // of the alert, the latestTimestamp will be used to gate the query in order to - // avoid counting a document multiple times. - - let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); - const filter = timestamp - ? { - bool: { - filter: [ - parsedQuery.query, - { - bool: { - must_not: [ - { - bool: { - filter: [ - { - range: { - [params.timeField]: { - lte: timestamp, - format: 'strict_date_optional_time', - }, - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - } - : parsedQuery.query; - - const query = buildSortedEventsQuery({ - index: params.index, - from: dateStart, - to: dateEnd, - filter, - size: params.size, - sortOrder: 'desc', - searchAfterSortId: undefined, - timeField: params.timeField, - track_total_hits: true, - }); - - logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`); - - const { body: searchResult } = await abortableEsClient.search(query); - - logger.debug( - `alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` - ); - - const numMatches = (searchResult.hits.total as estypes.SearchTotalHits).value; - - // apply the alert condition - const conditionMet = compareFn(numMatches, params.threshold); - - if (conditionMet) { - const humanFn = i18n.translate( - 'xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', - { - defaultMessage: `Number of matching documents is {thresholdComparator} {threshold}`, - values: { - thresholdComparator: getHumanReadableComparator(params.thresholdComparator), - threshold: params.threshold.join(' and '), - }, - } - ); - - const baseContext: EsQueryAlertActionContext = { - date: new Date().toISOString(), - value: numMatches, - conditions: humanFn, - hits: searchResult.hits.hits, - }; - - const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); - alertInstance - // store the params we would need to recreate the query that led to this alert instance - .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) - .scheduleActions(ActionGroupId, actionContext); - - // update the timestamp based on the current search results - const firstValidTimefieldSort = getValidTimefieldSort( - searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort - ); - if (firstValidTimefieldSort) { - timestamp = firstValidTimefieldSort; - } - } - - return { - latestTimestamp: timestamp, - }; -} - -async function indexThresholdExpressionExecutor( - logger: Logger, - core: CoreSetup, - options: ExecutorOptions -) { - const { name, params, alertId, state, services } = options; - const { timeField } = params; - const timestamp = new Date().toISOString(); - const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; - logger.debug( - `searchThreshold (${alertId}) previousTimestamp: ${state.previousTimestamp}, previousTimeRange ${state.previousTimeRange}` - ); - const compareFn = ComparatorFns.get(params.thresholdComparator); - if (compareFn == null) { - throw new Error( - i18n.translate('xpack.stackAlerts.searchThreshold.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator: params.thresholdComparator, - }, - }) - ); - } - - const searchSourceClient = await services.searchSourceClient; - const loadedSearchSource = await searchSourceClient.create(params.searchConfiguration); - const index = loadedSearchSource.getField('index'); - - loadedSearchSource.setField('size', 0); - - const filter = getTime(index, { - from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, - to: 'now', - }); - const from = filter?.query.range[timeField].gte; - const to = filter?.query.range[timeField].lte; - const searchSourceChild = loadedSearchSource.createChild(); - searchSourceChild.setField('filter', filter); - - let nrOfDocs = 0; - try { - logger.info( - `searchThreshold (${alertId}) query: ${JSON.stringify( - searchSourceChild.getSearchRequestBody() - )}` - ); - const docs = await searchSourceChild.fetch(); - nrOfDocs = Number(docs.hits.total); - logger.info(`searchThreshold (${alertId}) nrOfDocs: ${nrOfDocs}`); - } catch (error) { - logger.error('Error fetching documents: ' + error.message); - throw error; - } - - const met = compareFn(nrOfDocs, params.threshold); - - if (met) { - const conditions = `${nrOfDocs} is ${getHumanReadableComparator(params.thresholdComparator)} ${ - params.threshold - }`; - const checksum = sha256.create().update(JSON.stringify(params)); - const link = `${publicBaseUrl}/app/discover#/viewAlert/${alertId}?from=${from}&to=${to}&checksum=${checksum}`; - const baseContext: ActionContext = { - title: name, - message: `${nrOfDocs} documents found between ${from} and ${to}`, - date: timestamp, - value: Number(nrOfDocs), - conditions, - link, - }; - const alertInstance = options.services.alertFactory.create(ConditionMetAlertInstanceId); - alertInstance.scheduleActions(ActionGroupId, baseContext); - } - - // this is the state that we can access in the next execution - return { - latestTimestamp: timestamp, - }; +function isEsQueryAlert(options: ExecutorOptions) { + return options.params.searchType === 'esQuery'; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index e8cac552edcd7..1b275938986e7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -20,28 +20,38 @@ export interface EsQueryAlertState extends AlertTypeState { } export const EsQueryAlertParamsSchemaProperties = { - index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), - timeField: schema.string({ minLength: 1 }), - + size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), + timeWindowSize: schema.number({ min: 1 }), + timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), + threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), + thresholdComparator: schema.string({ validate: validateComparator }), + searchType: schema.oneOf([schema.literal('esQuery'), schema.literal('searchSource')]), + // searchSource alert param only + searchConfiguration: schema.conditional( + schema.siblingRef('searchType'), + schema.literal('searchSource'), + schema.object({}, { unknowns: 'allow' }), + schema.never() + ), + // esQuery alert params only esQuery: schema.conditional( schema.siblingRef('searchType'), - schema.literal(''), + schema.literal('esQuery'), schema.string({ minLength: 1 }), schema.never() ), - size: schema.conditional( + index: schema.conditional( schema.siblingRef('searchType'), schema.literal('esQuery'), - schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), + schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), + schema.never() + ), + timeField: schema.conditional( + schema.siblingRef('searchType'), + schema.literal('esQuery'), + schema.string({ minLength: 1 }), schema.never() ), - - timeWindowSize: schema.number({ min: 1 }), - timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), - threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), - thresholdComparator: schema.string({ validate: validateComparator }), - searchType: schema.oneOf([schema.literal('esQuery'), schema.literal('searchSource')]), - searchConfiguration: schema.object({}, { unknowns: 'allow' }), }; export const EsQueryAlertParamsSchema = schema.object(EsQueryAlertParamsSchemaProperties, { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/constants.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/constants.ts new file mode 100644 index 0000000000000..700cba4680bff --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/constants.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export const ES_QUERY_ID = '.es-query'; +export const ActionGroupId = 'query matched'; +export const ConditionMetAlertInstanceId = 'query matched'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/es_query_executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/es_query_executor.ts new file mode 100644 index 0000000000000..51c55fbcf1db4 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/es_query_executor.ts @@ -0,0 +1,208 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { Logger } from 'src/core/server'; +import { EsQueryAlertActionContext, addMessages } from '../action_context'; +import { ComparatorFns, getHumanReadableComparator } from '../../lib'; +import { parseDuration } from '../../../../../alerting/server'; +import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query'; +import { ExecutorOptions, OnlyEsQueryAlertParams } from '../types'; +import { ActionGroupId, ConditionMetAlertInstanceId, ES_QUERY_ID } from '../constants'; + +export async function esQueryExecutor( + logger: Logger, + options: ExecutorOptions +) { + const { alertId, name, services, params, state } = options; + const { alertFactory, search } = services; + const previousTimestamp = state.latestTimestamp; + + const abortableEsClient = search.asCurrentUser; + const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); + + const compareFn = ComparatorFns.get(params.thresholdComparator); + if (compareFn == null) { + throw new Error(getInvalidComparatorError(params.thresholdComparator)); + } + + // During each alert execution, we run the configured query, get a hit count + // (hits.total) and retrieve up to params.size hits. We + // evaluate the threshold condition using the value of hits.total. If the threshold + // condition is met, the hits are counted toward the query match and we update + // the alert state with the timestamp of the latest hit. In the next execution + // of the alert, the latestTimestamp will be used to gate the query in order to + // avoid counting a document multiple times. + + let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); + const filter = timestamp + ? { + bool: { + filter: [ + parsedQuery.query, + { + bool: { + must_not: [ + { + bool: { + filter: [ + { + range: { + [params.timeField]: { + lte: timestamp, + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + } + : parsedQuery.query; + + const query = buildSortedEventsQuery({ + index: params.index, + from: dateStart, + to: dateEnd, + filter, + size: params.size, + sortOrder: 'desc', + searchAfterSortId: undefined, + timeField: params.timeField, + track_total_hits: true, + }); + + logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`); + + const { body: searchResult } = await abortableEsClient.search(query); + + logger.debug( + `alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` + ); + + const numMatches = (searchResult.hits.total as estypes.SearchTotalHits).value; + + // apply the alert condition + const conditionMet = compareFn(numMatches, params.threshold); + + if (conditionMet) { + const humanFn = i18n.translate( + 'xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', + { + defaultMessage: `Number of matching documents is {thresholdComparator} {threshold}`, + values: { + thresholdComparator: getHumanReadableComparator(params.thresholdComparator), + threshold: params.threshold.join(' and '), + }, + } + ); + + const baseContext: EsQueryAlertActionContext = { + date: new Date().toISOString(), + value: numMatches, + conditions: humanFn, + hits: searchResult.hits.hits, + }; + + const actionContext = addMessages(options, baseContext, params); + const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); + alertInstance + // store the params we would need to recreate the query that led to this alert instance + .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) + .scheduleActions(ActionGroupId, actionContext); + + // update the timestamp based on the current search results + const firstValidTimefieldSort = getValidTimefieldSort( + searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort + ); + if (firstValidTimefieldSort) { + timestamp = firstValidTimefieldSort; + } + } + + return { + latestTimestamp: timestamp, + }; +} + +function getInvalidComparatorError(comparator: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); +} + +function getInvalidWindowSizeError(windowValue: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', { + defaultMessage: 'invalid format for windowSize: "{windowValue}"', + values: { + windowValue, + }, + }); +} + +function getInvalidQueryError(query: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', { + defaultMessage: 'invalid query specified: "{query}" - query must be JSON', + values: { + query, + }, + }); +} + +function getSearchParams(queryParams: OnlyEsQueryAlertParams) { + const date = Date.now(); + const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; + + let parsedQuery; + try { + parsedQuery = JSON.parse(esQuery); + } catch (err) { + throw new Error(getInvalidQueryError(esQuery)); + } + + if (parsedQuery && !parsedQuery.query) { + throw new Error(getInvalidQueryError(esQuery)); + } + + const window = `${timeWindowSize}${timeWindowUnit}`; + let timeWindow: number; + try { + timeWindow = parseDuration(window); + } catch (err) { + throw new Error(getInvalidWindowSizeError(window)); + } + + const dateStart = new Date(date - timeWindow).toISOString(); + const dateEnd = new Date(date).toISOString(); + + return { parsedQuery, dateStart, dateEnd }; +} + +function getValidTimefieldSort(sortValues: Array = []): undefined | string { + for (const sortValue of sortValues) { + const sortDate = tryToParseAsDate(sortValue); + if (sortDate) { + return sortDate; + } + } +} + +function tryToParseAsDate(sortValue?: string | number | null): undefined | string { + const sortDate = typeof sortValue === 'string' ? Date.parse(sortValue) : sortValue; + if (sortDate && !isNaN(sortDate)) { + return new Date(sortDate).toISOString(); + } +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/index.ts new file mode 100644 index 0000000000000..67740338a6ef4 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { esQueryExecutor } from './es_query_executor'; +import { searchSourceExecutor } from './search_source_executor'; + +export { esQueryExecutor, searchSourceExecutor }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/search_source_executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/search_source_executor.ts new file mode 100644 index 0000000000000..3e17834a2125f --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/search_source_executor.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { sha256 } from 'js-sha256'; +import { CoreSetup, Logger } from 'src/core/server'; +import { getTime } from '../../../../../../../src/plugins/data/common'; +import { ActionContext } from '../action_context'; +import { ComparatorFns, getHumanReadableComparator } from '../../lib'; +import { ExecutorOptions, OnlySearchSourceAlertParams } from '../types'; +import { ActionGroupId, ConditionMetAlertInstanceId } from '../constants'; + +export async function searchSourceExecutor( + logger: Logger, + core: CoreSetup, + options: ExecutorOptions +) { + const { name, params, alertId, state, services } = options; + const timestamp = new Date().toISOString(); + const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; + + logger.debug( + `searchThreshold (${alertId}) previousTimestamp: ${state.previousTimestamp}, previousTimeRange ${state.previousTimeRange}` + ); + + const compareFn = ComparatorFns.get(params.thresholdComparator); + if (compareFn == null) { + throw new Error( + i18n.translate('xpack.stackAlerts.searchThreshold.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator: params.thresholdComparator, + }, + }) + ); + } + + const searchSourceClient = await services.searchSourceClient; + const loadedSearchSource = await searchSourceClient.create(params.searchConfiguration); + const index = loadedSearchSource.getField('index'); + + const timeFieldName = index?.timeFieldName; + if (!timeFieldName) { + throw new Error('Invalid data view without timeFieldName'); + } + + loadedSearchSource.setField('size', params.size); + + const filter = getTime(index, { + from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, + to: 'now', + }); + const from = filter?.query.range[timeFieldName].gte; + const to = filter?.query.range[timeFieldName].lte; + const searchSourceChild = loadedSearchSource.createChild(); + searchSourceChild.setField('filter', filter); + + let nrOfDocs = 0; + let searchResult; + try { + logger.info( + `searchThreshold (${alertId}) query: ${JSON.stringify( + searchSourceChild.getSearchRequestBody() + )}` + ); + searchResult = await searchSourceChild.fetch(); + nrOfDocs = Number(searchResult.hits.total); + logger.info(`searchThreshold (${alertId}) nrOfDocs: ${nrOfDocs}`); + } catch (error) { + logger.error('Error fetching documents: ' + error.message); + throw error; + } + + const met = compareFn(nrOfDocs, params.threshold); + + if (met) { + const conditions = `${nrOfDocs} is ${getHumanReadableComparator(params.thresholdComparator)} ${ + params.threshold + }`; + const checksum = sha256.create().update(JSON.stringify(params)); + const link = `${publicBaseUrl}/app/discover#/viewAlert/${alertId}?from=${from}&to=${to}&checksum=${checksum}`; + const baseContext: ActionContext = { + title: name, + message: `${nrOfDocs} documents found between ${from} and ${to}`, + date: timestamp, + value: Number(nrOfDocs), + conditions, + link, + hits: searchResult.hits.hits, + }; + const alertInstance = options.services.alertFactory.create(ConditionMetAlertInstanceId); + alertInstance.scheduleActions(ActionGroupId, baseContext); + } + + // this is the state that we can access in the next execution + return { + latestTimestamp: timestamp, + }; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts new file mode 100644 index 0000000000000..512ac3d8cd359 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts @@ -0,0 +1,30 @@ +/* + * 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 { AlertExecutorOptions, AlertTypeParams } from '../../types'; +import { ActionContext } from './action_context'; +import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; +import { ActionGroupId } from './constants'; + +export type OnlyEsQueryAlertParams = Omit & { + searchType: 'esQuery'; +}; + +export type OnlySearchSourceAlertParams = Omit< + EsQueryAlertParams, + 'esQuery' | 'index' | 'timeField' +> & { + searchType: 'searchSource'; +}; + +export type ExecutorOptions

= AlertExecutorOptions< + P, + EsQueryAlertState, + {}, + ActionContext, + typeof ActionGroupId +>; diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 39ea41374df7b..166f2103526ef 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { KibanaFeatureConfig } from '../../../plugins/features/common'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; -import { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/alert_type'; +import { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/constants'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { TRANSFORM_RULE_TYPE } from '../../transform/common'; diff --git a/x-pack/plugins/stack_alerts/server/types.ts b/x-pack/plugins/stack_alerts/server/types.ts index 6422389fefbe3..719373d21f18a 100644 --- a/x-pack/plugins/stack_alerts/server/types.ts +++ b/x-pack/plugins/stack_alerts/server/types.ts @@ -13,6 +13,7 @@ export type { RuleType, RuleParamsAndRefs, AlertExecutorOptions, + AlertTypeParams, } from '../../alerting/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; From 153023b961b7d0a6f5e49e205e1ef2990881adee Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Sun, 13 Feb 2022 22:48:05 +0300 Subject: [PATCH 06/67] [Discover] add view alert route --- .../public/application/discover_router.tsx | 4 + .../public/application/view_alert/index.ts | 9 ++ .../view_alert/view_alert_route.tsx | 153 ++++++++++++++++++ src/plugins/discover/public/build_services.ts | 6 +- src/plugins/discover/public/plugin.tsx | 9 +- 5 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 src/plugins/discover/public/application/view_alert/index.ts create mode 100644 src/plugins/discover/public/application/view_alert/view_alert_route.tsx diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx index 16ff443d15d24..0270af2383488 100644 --- a/src/plugins/discover/public/application/discover_router.tsx +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -16,6 +16,7 @@ import { SingleDocRoute } from './doc'; import { DiscoverMainRoute } from './main'; import { NotFoundRoute } from './not_found'; import { DiscoverServices } from '../build_services'; +import { ViewAlertRoute } from './view_alert'; export const discoverRouter = (services: DiscoverServices, history: History) => ( @@ -36,6 +37,9 @@ export const discoverRouter = (services: DiscoverServices, history: History) => + + + diff --git a/src/plugins/discover/public/application/view_alert/index.ts b/src/plugins/discover/public/application/view_alert/index.ts new file mode 100644 index 0000000000000..9b3e4f5d3bf7e --- /dev/null +++ b/src/plugins/discover/public/application/view_alert/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { ViewAlertRoute } from './view_alert_route'; diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx new file mode 100644 index 0000000000000..2df7e706bcf71 --- /dev/null +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useEffect } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { sha256 } from 'js-sha256'; +import { i18n } from '@kbn/i18n'; +import { ToastsStart } from 'kibana/public'; +import type { Alert } from '../../../../../../x-pack/plugins/alerting/common'; +import { AlertTypeParams } from '../../../../../../x-pack/plugins/alerting/common'; +import { Filter, SerializedSearchSourceFields } from '../../../../data/common'; +import { MarkdownSimple, toMountPoint } from '../../../../kibana_react/public'; +import { DiscoverAppLocatorParams } from '../../locator'; +import { withQueryParams } from '../../utils/with_query_params'; +import { useDiscoverServices } from '../../utils/use_discover_services'; + +interface SearchThresholdAlertParams extends AlertTypeParams { + searchConfiguration: SerializedSearchSourceFields; +} + +interface ViewAlertProps { + from: string; + to: string; + checksum: string; +} + +const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; +const DISCOVER_MAIN_ROUTE = '/'; + +const displayError = (title: string, errorMessage: string, toastNotifications: ToastsStart) => { + toastNotifications.addDanger({ + title, + text: toMountPoint({errorMessage}), + }); +}; + +const displayRuleChangedWarn = (toastNotifications: ToastsStart) => { + const warnTitle = i18n.translate('discover.viewAlert.alertRuleChangedWarnTitle', { + defaultMessage: 'Alert rule has changed', + }); + const warnDescription = i18n.translate('discover.viewAlert.alertRuleChangedWarnDescription', { + defaultMessage: `Displayed documents might not match the documents triggered notification, + since the rule configuration has been changed.`, + }); + + toastNotifications.addWarning({ + title: warnTitle, + text: toMountPoint({warnDescription}), + }); +}; + +const getCurrentChecksum = (params: SearchThresholdAlertParams) => + sha256.create().update(JSON.stringify(params)).hex(); + +function ViewAlert({ from, to, checksum }: ViewAlertProps) { + const { core, data, locator, toastNotifications } = useDiscoverServices(); + const { id } = useParams<{ id: string }>(); + const history = useHistory(); + + useEffect(() => { + const fetchAlert = async () => { + try { + return await core.http.get>( + `${LEGACY_BASE_ALERT_API_PATH}/alert/${id}` + ); + } catch (error) { + const errorTitle = i18n.translate('discover.viewAlert.alertRuleFetchErrorTitle', { + defaultMessage: 'Alert rule fetch error', + }); + displayError(errorTitle, error.message, toastNotifications); + } + }; + + const fetchSearchSource = async (fetchedAlert: Alert) => { + try { + return await data.search.searchSource.create(fetchedAlert.params.searchConfiguration); + } catch (error) { + const errorTitle = i18n.translate('discover.viewAlert.searchSourceErrorTitle', { + defaultMessage: 'Search source fetch error', + }); + displayError(errorTitle, error.message, toastNotifications); + } + }; + + const showDataViewFetchError = (alertId: string) => { + const errorTitle = i18n.translate('discover.viewAlert.dataViewErrorTitle', { + defaultMessage: 'Data view fetch error', + }); + displayError( + errorTitle, + new Error(`Can't find data view of the alert rule with id ${alertId}.`).message, + toastNotifications + ); + }; + + const navigateToResults = async () => { + const fetchedAlert = await fetchAlert(); + if (!fetchedAlert) { + history.push(DISCOVER_MAIN_ROUTE); + return; + } + + if (getCurrentChecksum(fetchedAlert.params) !== checksum) { + displayRuleChangedWarn(toastNotifications); + } + + const fetchedSearchSource = await fetchSearchSource(fetchedAlert); + if (!fetchedSearchSource) { + history.push(DISCOVER_MAIN_ROUTE); + return; + } + + const dataView = fetchedSearchSource.getField('index'); + if (!dataView) { + showDataViewFetchError(fetchedAlert.id); + history.push(DISCOVER_MAIN_ROUTE); + return; + } + + const state: DiscoverAppLocatorParams = { + query: fetchedSearchSource.getField('query') || data.query.queryString.getDefaultQuery(), + indexPatternId: dataView.id, + timeRange: { from, to }, + }; + const filters = fetchedSearchSource.getField('filter'); + if (filters) { + state.filters = filters as Filter[]; + } + await locator.navigate(state); + }; + + navigateToResults(); + }, [ + toastNotifications, + data.query.queryString, + data.search.searchSource, + core.http, + locator, + id, + checksum, + from, + to, + history, + ]); + + return null; +} + +export const ViewAlertRoute = withQueryParams(ViewAlert, ['from', 'to', 'checksum']); diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 3d4f74c6d9ffc..79726b40363ec 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -40,6 +40,7 @@ import { NavigationPublicPluginStart } from '../../navigation/public'; import { IndexPatternFieldEditorStart } from '../../data_view_field_editor/public'; import { FieldFormatsStart } from '../../field_formats/public'; import { EmbeddableStart } from '../../embeddable/public'; +import { DiscoverAppLocator } from './locator'; import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public'; import { DataViewEditorStart } from '../../../plugins/data_view_editor/public'; @@ -79,12 +80,14 @@ export interface DiscoverServices { storage: Storage; spaces?: SpacesApi; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; + locator: DiscoverAppLocator; } export const buildServices = memoize(function ( core: CoreStart, plugins: DiscoverStartPlugins, - context: PluginInitializerContext + context: PluginInitializerContext, + locator: DiscoverAppLocator ): DiscoverServices { const { usageCollection } = plugins; const storage = new Storage(localStorage); @@ -121,5 +124,6 @@ export const buildServices = memoize(function ( spaces: plugins.spaces, dataViewEditor: plugins.dataViewEditor, triggersActionsUi: plugins.triggersActionsUi, + locator, }; }); diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index c9219a97fbd3e..306a8f20f9eaf 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -353,7 +353,12 @@ export class DiscoverPlugin }); const [coreStart, discoverStartPlugins] = await core.getStartServices(); - const services = buildServices(coreStart, discoverStartPlugins, this.initializerContext); + const services = buildServices( + coreStart, + discoverStartPlugins, + this.initializerContext, + this.locator! + ); // make sure the index pattern list is up to date await discoverStartPlugins.data.indexPatterns.clearCache(); @@ -444,7 +449,7 @@ export class DiscoverPlugin const getDiscoverServices = async () => { const [coreStart, discoverStartPlugins] = await core.getStartServices(); - return buildServices(coreStart, discoverStartPlugins, this.initializerContext); + return buildServices(coreStart, discoverStartPlugins, this.initializerContext, this.locator!); }; const factory = new SearchEmbeddableFactory(getStartServices, getDiscoverServices); From 37654ffedd3a04948d757830dd0bf67309b54430 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Sat, 19 Feb 2022 22:55:58 +0300 Subject: [PATCH 07/67] [Discover] enable "view in app" for alert created from discover --- .../view_alert/view_alert_route.tsx | 70 ++++++++++++++----- .../public/alert_types/es_query/index.ts | 23 +++++- .../stack_alerts/public/alert_types/index.ts | 5 +- x-pack/plugins/stack_alerts/public/plugin.tsx | 5 +- .../server/alert_types/es_query/alert_type.ts | 29 +++++++- 5 files changed, 107 insertions(+), 25 deletions(-) diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 2df7e706bcf71..856e11cda66b6 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -5,29 +5,31 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useEffect } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; +import React, { useEffect, useMemo } from 'react'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; import { sha256 } from 'js-sha256'; import { i18n } from '@kbn/i18n'; import { ToastsStart } from 'kibana/public'; import type { Alert } from '../../../../../../x-pack/plugins/alerting/common'; import { AlertTypeParams } from '../../../../../../x-pack/plugins/alerting/common'; -import { Filter, SerializedSearchSourceFields } from '../../../../data/common'; +import { getTime, SerializedSearchSourceFields } from '../../../../data/common'; +import type { Filter, TimeRange } from '../../../../data/public'; import { MarkdownSimple, toMountPoint } from '../../../../kibana_react/public'; import { DiscoverAppLocatorParams } from '../../locator'; -import { withQueryParams } from '../../utils/with_query_params'; import { useDiscoverServices } from '../../utils/use_discover_services'; interface SearchThresholdAlertParams extends AlertTypeParams { searchConfiguration: SerializedSearchSourceFields; } -interface ViewAlertProps { - from: string; - to: string; - checksum: string; +interface QueryParams { + from: string | null; + to: string | null; + checksum: string | null; } +type NonNullableEntry = { [K in keyof T]: NonNullable }; + const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; const DISCOVER_MAIN_ROUTE = '/'; @@ -56,10 +58,30 @@ const displayRuleChangedWarn = (toastNotifications: ToastsStart) => { const getCurrentChecksum = (params: SearchThresholdAlertParams) => sha256.create().update(JSON.stringify(params)).hex(); -function ViewAlert({ from, to, checksum }: ViewAlertProps) { +const isConcreteAlert = ( + queryParams: QueryParams +): queryParams is NonNullableEntry => { + return Boolean(queryParams.from && queryParams.to && queryParams.checksum); +}; + +export function ViewAlertRoute() { const { core, data, locator, toastNotifications } = useDiscoverServices(); const { id } = useParams<{ id: string }>(); const history = useHistory(); + const { search } = useLocation(); + + const query = useMemo(() => new URLSearchParams(search), [search]); + + const queryParams: QueryParams = useMemo( + () => ({ + from: query.get('from'), + to: query.get('to'), + checksum: query.get('checksum'), + }), + [query] + ); + + const openConcreteAlert = isConcreteAlert(queryParams); useEffect(() => { const fetchAlert = async () => { @@ -92,7 +114,7 @@ function ViewAlert({ from, to, checksum }: ViewAlertProps) { }); displayError( errorTitle, - new Error(`Can't find data view of the alert rule with id ${alertId}.`).message, + new Error(`Data view failure of the alert rule with id ${alertId}.`).message, toastNotifications ); }; @@ -104,7 +126,7 @@ function ViewAlert({ from, to, checksum }: ViewAlertProps) { return; } - if (getCurrentChecksum(fetchedAlert.params) !== checksum) { + if (openConcreteAlert && getCurrentChecksum(fetchedAlert.params) !== queryParams.checksum) { displayRuleChangedWarn(toastNotifications); } @@ -115,16 +137,31 @@ function ViewAlert({ from, to, checksum }: ViewAlertProps) { } const dataView = fetchedSearchSource.getField('index'); - if (!dataView) { + const timeFieldName = dataView?.timeFieldName; + if (!dataView || !timeFieldName) { showDataViewFetchError(fetchedAlert.id); history.push(DISCOVER_MAIN_ROUTE); return; } + let timeRange: TimeRange; + if (openConcreteAlert) { + timeRange = { from: queryParams.from, to: queryParams.to }; + } else { + const filter = getTime(dataView, { + from: `now-${fetchedAlert.params.timeWindowSize}${fetchedAlert.params.timeWindowUnit}`, + to: 'now', + }); + timeRange = { + from: filter?.query.range[timeFieldName].gte, + to: filter?.query.range[timeFieldName].lte, + }; + } + const state: DiscoverAppLocatorParams = { query: fetchedSearchSource.getField('query') || data.query.queryString.getDefaultQuery(), indexPatternId: dataView.id, - timeRange: { from, to }, + timeRange, }; const filters = fetchedSearchSource.getField('filter'); if (filters) { @@ -141,13 +178,10 @@ function ViewAlert({ from, to, checksum }: ViewAlertProps) { core.http, locator, id, - checksum, - from, - to, + queryParams, history, + openConcreteAlert, ]); return null; } - -export const ViewAlertRoute = withQueryParams(ViewAlert, ['from', 'to', 'checksum']); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts index cf54c5934c026..795824389caf8 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts @@ -8,12 +8,19 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { validateExpression } from './validation'; -import { EsQueryAlertParams } from './types'; +import { EsQueryAlertParams, SearchType } from './types'; import { RuleTypeModel } from '../../../../triggers_actions_ui/public'; +import { PluginSetupContract as AlertingSetup } from '../../../../alerting/public'; +import { SanitizedAlert } from '../../../../alerting/common'; + +const PLUGIN_ID = 'discover'; +const ES_QUERY_ALERT_TYPE = '.es-query'; + +export function getAlertType(alerting: AlertingSetup): RuleTypeModel { + registerNavigation(alerting); -export function getAlertType(): RuleTypeModel { return { - id: '.es-query', + id: ES_QUERY_ALERT_TYPE, description: i18n.translate('xpack.stackAlerts.esQuery.ui.alertType.descriptionText', { defaultMessage: 'Alert when matches are found during the latest query run.', }), @@ -34,3 +41,13 @@ export function getAlertType(): RuleTypeModel { requiresAppContext: false, }; } + +function registerNavigation(alerting: AlertingSetup) { + alerting.registerNavigation( + PLUGIN_ID, + ES_QUERY_ALERT_TYPE, + (alert: SanitizedAlert>) => { + return `#/viewAlert/${alert.id}`; + } + ); +} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts index d2f0f1860417b..c0a0f7b6548cf 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -10,15 +10,18 @@ import { getAlertType as getThresholdAlertType } from './threshold'; import { getAlertType as getEsQueryAlertType } from './es_query'; import { Config } from '../../common'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; +import { PluginSetupContract as AlertingSetup } from '../../../alerting/public'; export function registerAlertTypes({ ruleTypeRegistry, config, + alerting, }: { ruleTypeRegistry: TriggersAndActionsUIPublicPluginSetup['ruleTypeRegistry']; config: Config; + alerting: AlertingSetup; }) { ruleTypeRegistry.register(getGeoContainmentAlertType()); ruleTypeRegistry.register(getThresholdAlertType()); - ruleTypeRegistry.register(getEsQueryAlertType()); + ruleTypeRegistry.register(getEsQueryAlertType(alerting)); } diff --git a/x-pack/plugins/stack_alerts/public/plugin.tsx b/x-pack/plugins/stack_alerts/public/plugin.tsx index f636139571ca8..8d004523558fb 100644 --- a/x-pack/plugins/stack_alerts/public/plugin.tsx +++ b/x-pack/plugins/stack_alerts/public/plugin.tsx @@ -9,12 +9,14 @@ import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; import { registerAlertTypes } from './alert_types'; import { Config } from '../common'; +import { PluginSetupContract as AlertingSetup } from '../../alerting/public'; export type Setup = void; export type Start = void; export interface StackAlertsPublicSetupDeps { triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; + alerting: AlertingSetup; } export class StackAlertsPublicPlugin implements Plugin { @@ -24,10 +26,11 @@ export class StackAlertsPublicPlugin implements Plugin(), + alerting, }); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 0ff4af8979871..9e2f1274b5ba1 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -116,6 +116,28 @@ export function getAlertType( } ); + const actionVariableSearchTypeLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextSearchTypeLabel', + { + defaultMessage: 'The type of search is used.', + } + ); + + const actionVariableSearchConfigurationLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextSearchConfigurationLabel', + { + defaultMessage: + 'Serialized search source fields used to fetch the documents from Elasticsearch.', + } + ); + + const actionVariableContextLinkLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', + { + defaultMessage: 'A link to see records that triggered this alert.', + } + ); + return { id: ES_QUERY_ID, name: alertTypeName, @@ -132,13 +154,16 @@ export function getAlertType( { name: 'value', description: actionVariableContextValueLabel }, { name: 'hits', description: actionVariableContextHitsLabel }, { name: 'conditions', description: actionVariableContextConditionsLabel }, + { name: 'link', description: actionVariableContextLinkLabel }, ], params: [ - { name: 'index', description: actionVariableContextIndexLabel }, - { name: 'esQuery', description: actionVariableContextQueryLabel }, { name: 'size', description: actionVariableContextSizeLabel }, { name: 'threshold', description: actionVariableContextThresholdLabel }, { name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel }, + { name: 'searchType', description: actionVariableSearchTypeLabel }, + { name: 'searchConfiguration', description: actionVariableSearchConfigurationLabel }, + { name: 'esQuery', description: actionVariableContextQueryLabel }, + { name: 'index', description: actionVariableContextIndexLabel }, ], }, minimumLicenseRequired: 'basic', From 7088f4b3167a1e46955db76967bced4f08d63d36 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Sun, 20 Feb 2022 16:13:43 +0300 Subject: [PATCH 08/67] [Discover] fix filter popover --- .../ui/filter_bar/_global_filter_item.scss | 4 +++ .../filter_editor/lib/filter_label.tsx | 10 ++++-- .../data/public/ui/filter_bar/filter_item.tsx | 35 ++++++++++--------- .../ui/filter_bar/filter_view/index.tsx | 1 + 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 4873989cde638..3df9fb6cf2d99 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -88,3 +88,7 @@ .globalFilterItem__popoverAnchor { display: block; } + +.globalFilterItem__readonlyPanel { + min-width: auto; +} diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 1a272a5d79f37..b8ebd369afd49 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -17,11 +17,17 @@ export interface FilterLabelProps { filter: Filter; valueLabel?: string; filterLabelStatus?: FilterLabelStatus; + readonly?: boolean; } // Needed for React.lazy // eslint-disable-next-line import/no-default-export -export default function FilterLabel({ filter, valueLabel, filterLabelStatus }: FilterLabelProps) { +export default function FilterLabel({ + filter, + valueLabel, + filterLabelStatus, + readonly, +}: FilterLabelProps) { const prefixText = filter.meta.negate ? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', { defaultMessage: 'NOT ', @@ -38,7 +44,7 @@ export default function FilterLabel({ filter, valueLabel, filterLabelStatus }: F return {text}; }; - if (filter.meta.alias !== null) { + if (!readonly && filter.meta.alias !== null) { return ( {prefix} diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index e38ef9a835b3c..06ced8d59df90 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -350,19 +350,16 @@ export function FilterItem(props: FilterItemProps) { return null; } - const badge = ( - props.onRemove()} - onClick={handleBadgeClick} - data-test-subj={getDataTestSubj(valueLabelConfig)} - readonly={props.readonly} - /> - ); + const filterViewProps = { + filter, + valueLabel: valueLabelConfig.title, + filterLabelStatus: valueLabelConfig.status, + errorMessage: valueLabelConfig.message, + className: getClasses(filter.meta.negate ?? false, valueLabelConfig), + iconOnClick: () => props.onRemove(), + onClick: handleBadgeClick, + ['data-test-subj']: getDataTestSubj(valueLabelConfig), + }; const popoverProps: CommonProps & HTMLAttributes & EuiPopoverProps = { id: `popoverFor_filter${id}`, @@ -372,20 +369,24 @@ export function FilterItem(props: FilterItemProps) { closePopover: () => { setIsPopoverOpen(false); }, - button: badge, + button: , panelPaddingSize: 'none', }; if (props.readonly) { return ( - - {badge} + + ); } return ( - + ); diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 8066a56021247..82aa7c392e0b1 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -90,6 +90,7 @@ export const FilterView: FC = ({ filter={filter} valueLabel={valueLabel} filterLabelStatus={filterLabelStatus} + readonly={readonly} /> From b618cce9edf4b86c368e4a56bdd6961739f304c2 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 21 Feb 2022 12:38:41 +0300 Subject: [PATCH 09/67] [Discover] fix linting, unit tests --- .../data/public/ui/filter_bar/filter_bar.tsx | 7 +- .../filter_editor/lib/filter_label.tsx | 6 +- .../ui/filter_bar/filter_view/index.tsx | 2 +- .../data/public/ui/filter_bar/index.tsx | 8 -- .../data/public/ui/search_bar/index.tsx | 4 +- .../expression/search_source_expression.scss | 1 - .../es_query/action_context.test.ts | 7 +- .../alert_types/es_query/alert_type.test.ts | 76 ++++++++++++------- .../es_query/alert_type_params.test.ts | 1 + 9 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 302bd39c920fe..00557dfab0e98 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -30,7 +30,7 @@ import { IDataPluginServices, IIndexPattern } from '../..'; import { UI_SETTINGS } from '../../../common'; -export interface FilterBarProps { +export interface Props { filters: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; className: string; @@ -40,7 +40,7 @@ export interface FilterBarProps { timeRangeForSuggestionsOverride?: boolean; } -const FilterBarUI = React.memo(function FilterBarUI(props: FilterBarProps) { +const FilterBarUI = React.memo(function FilterBarUI(props: Props) { const groupRef = useRef(null); const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); const kibana = useKibana(); @@ -229,6 +229,3 @@ const FilterBarUI = React.memo(function FilterBarUI(props: FilterBarProps) { }); export const FilterBar = injectI18n(FilterBarUI); - -// eslint-disable-next-line import/no-default-export -export default FilterBar; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index b8ebd369afd49..d0924258831cb 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -17,7 +17,7 @@ export interface FilterLabelProps { filter: Filter; valueLabel?: string; filterLabelStatus?: FilterLabelStatus; - readonly?: boolean; + hideAlias?: boolean; } // Needed for React.lazy @@ -26,7 +26,7 @@ export default function FilterLabel({ filter, valueLabel, filterLabelStatus, - readonly, + hideAlias, }: FilterLabelProps) { const prefixText = filter.meta.negate ? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', { @@ -44,7 +44,7 @@ export default function FilterLabel({ return {text}; }; - if (!readonly && filter.meta.alias !== null) { + if (!hideAlias && filter.meta.alias !== null) { return ( {prefix} diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 82aa7c392e0b1..50e8e54975730 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -90,7 +90,7 @@ export const FilterView: FC = ({ filter={filter} valueLabel={valueLabel} filterLabelStatus={filterLabelStatus} - readonly={readonly} + hideAlias={readonly} /> diff --git a/src/plugins/data/public/ui/filter_bar/index.tsx b/src/plugins/data/public/ui/filter_bar/index.tsx index 32742e83930d6..b3c02b2863c83 100644 --- a/src/plugins/data/public/ui/filter_bar/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/index.tsx @@ -7,7 +7,6 @@ */ import React from 'react'; -import type { FilterBarProps } from './filter_bar'; const Fallback = () =>

; @@ -24,10 +23,3 @@ export const FilterItem = (props: React.ComponentProps) = ); - -const LazyFilterBar = React.lazy(() => import('./filter_bar')); -export const FilterBar = (props: FilterBarProps) => ( - }> - - -); diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index b31b53fd399f2..304855610f040 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -7,8 +7,6 @@ */ import React from 'react'; -import { injectI18n } from '@kbn/i18n-react'; -import { withKibana } from '../../../../kibana_react/public'; import type { SearchBarProps } from './search_bar'; const Fallback = () =>
; @@ -20,6 +18,6 @@ const WrappedSearchBar = (props: SearchBarProps) => ( ); -export const SearchBar = injectI18n(withKibana(WrappedSearchBar)); +export const SearchBar = WrappedSearchBar; export type { StatefulSearchBarProps } from './create_search_bar'; export type { SearchBarProps, SearchBarOwnProps } from './search_bar'; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss index d34383a2569dc..28401e6657e87 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss @@ -7,4 +7,3 @@ .dscExpressionParam.euiExpression { margin-left: 0; } - \ No newline at end of file diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts index 9d4edd83a3913..f9b5c1c692126 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -7,6 +7,7 @@ import { EsQueryAlertActionContext, addMessages } from './action_context'; import { EsQueryAlertParamsSchema } from './alert_type_params'; +import { OnlyEsQueryAlertParams } from './types'; describe('ActionContext', () => { it('generates expected properties', async () => { @@ -19,7 +20,8 @@ describe('ActionContext', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [4], - }); + searchType: 'esQuery', + }) as OnlyEsQueryAlertParams; const base: EsQueryAlertActionContext = { date: '2020-01-01T00:00:00.000Z', value: 42, @@ -47,7 +49,8 @@ describe('ActionContext', () => { timeWindowUnit: 'm', thresholdComparator: 'between', threshold: [4, 5], - }); + searchType: 'esQuery', + }) as OnlyEsQueryAlertParams; const base: EsQueryAlertActionContext = { date: '2020-01-01T00:00:00.000Z', value: 4, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 307496e2be391..3d38c1ba591b3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -14,17 +14,20 @@ import { AlertInstanceMock, } from '../../../../alerting/server/mocks'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; -import { getAlertType, ConditionMetAlertInstanceId, ActionGroupId } from './alert_type'; +import { getAlertType } from './alert_type'; import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; import { ActionContext } from './action_context'; import { ESSearchResponse, ESSearchRequest } from '../../../../../../src/core/types/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; +import { coreMock } from '../../../../../../src/core/server/mocks'; +import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; +import { OnlyEsQueryAlertParams } from './types'; describe('alertType', () => { const logger = loggingSystemMock.create().get(); - - const alertType = getAlertType(logger); + const coreSetup = coreMock.createSetup(); + const alertType = getAlertType(logger, coreSetup); it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.es-query'); @@ -58,16 +61,12 @@ describe('alertType', () => { "description": "A string that describes the threshold condition.", "name": "conditions", }, - ], - "params": Array [ Object { - "description": "The index the query was run against.", - "name": "index", - }, - Object { - "description": "The string representation of the Elasticsearch query.", - "name": "esQuery", + "description": "A link to see records that triggered this alert.", + "name": "link", }, + ], + "params": Array [ Object { "description": "The number of hits to retrieve for each query.", "name": "size", @@ -80,13 +79,29 @@ describe('alertType', () => { "description": "A function to determine if the threshold has been met.", "name": "thresholdComparator", }, + Object { + "description": "The type of search is used.", + "name": "searchType", + }, + Object { + "description": "Serialized search source fields used to fetch the documents from Elasticsearch.", + "name": "searchConfiguration", + }, + Object { + "description": "The string representation of the Elasticsearch query.", + "name": "esQuery", + }, + Object { + "description": "The index the query was run against.", + "name": "index", + }, ], } `); }); it('validator succeeds with valid params', async () => { - const params: Partial> = { + const params: Partial> = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -95,6 +110,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: '<', threshold: [0], + searchType: 'esQuery', }; expect(alertType.validate?.params?.validate(params)).toBeTruthy(); @@ -104,7 +120,7 @@ describe('alertType', () => { const paramsSchema = alertType.validate?.params; if (!paramsSchema) throw new Error('params validator not set'); - const params: Partial> = { + const params: Partial> = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -113,6 +129,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: 'between', threshold: [0], + searchType: 'esQuery', }; expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( @@ -121,7 +138,7 @@ describe('alertType', () => { }); it('alert executor handles no documentes returned by ES', async () => { - const params: EsQueryAlertParams = { + const params: OnlyEsQueryAlertParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -130,6 +147,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: 'between', threshold: [0], + searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -148,7 +166,7 @@ describe('alertType', () => { ActionContext, typeof ActionGroupId >, - params, + params: params as EsQueryAlertParams, state: { latestTimestamp: undefined, }, @@ -188,7 +206,7 @@ describe('alertType', () => { }); it('alert executor returns the latestTimestamp of the newest detected document', async () => { - const params: EsQueryAlertParams = { + const params: OnlyEsQueryAlertParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -197,6 +215,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [0], + searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -227,7 +246,7 @@ describe('alertType', () => { ActionContext, typeof ActionGroupId >, - params, + params: params as EsQueryAlertParams, state: { latestTimestamp: undefined, }, @@ -271,7 +290,7 @@ describe('alertType', () => { }); it('alert executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => { - const params: EsQueryAlertParams = { + const params: OnlyEsQueryAlertParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -280,6 +299,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [0], + searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -342,7 +362,7 @@ describe('alertType', () => { }); it('alert executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => { - const params: EsQueryAlertParams = { + const params: OnlyEsQueryAlertParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -351,6 +371,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [0], + searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -379,7 +400,7 @@ describe('alertType', () => { ActionContext, typeof ActionGroupId >, - params, + params: params as EsQueryAlertParams, state: { // inaalid legacy `latestTimestamp` latestTimestamp: 'FaslK3QBySSL_rrj9zM5', @@ -423,7 +444,7 @@ describe('alertType', () => { }); it('alert executor carries over the queried latestTimestamp in the alert state', async () => { - const params: EsQueryAlertParams = { + const params: OnlyEsQueryAlertParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -432,6 +453,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [0], + searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -457,7 +479,7 @@ describe('alertType', () => { ActionContext, typeof ActionGroupId >, - params, + params: params as EsQueryAlertParams, state: { latestTimestamp: undefined, }, @@ -531,7 +553,7 @@ describe('alertType', () => { }); it('alert executor ignores tie breaker sort values', async () => { - const params: EsQueryAlertParams = { + const params: OnlyEsQueryAlertParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -540,6 +562,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [0], + searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -571,7 +594,7 @@ describe('alertType', () => { ActionContext, typeof ActionGroupId >, - params, + params: params as EsQueryAlertParams, state: { latestTimestamp: undefined, }, @@ -614,7 +637,7 @@ describe('alertType', () => { }); it('alert executor ignores results with no sort values', async () => { - const params: EsQueryAlertParams = { + const params: OnlyEsQueryAlertParams = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, @@ -623,6 +646,7 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [0], + searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -655,7 +679,7 @@ describe('alertType', () => { ActionContext, typeof ActionGroupId >, - params, + params: params as EsQueryAlertParams, state: { latestTimestamp: undefined, }, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts index ab3ca6a2d4c31..c0ad883271e9e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts @@ -22,6 +22,7 @@ const DefaultParams: Writable> = { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [0], + searchType: 'esQuery', }; describe('alertType Params validate()', () => { From b46d24df4b457253243538f96f77086b164ec4aa Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 21 Feb 2022 14:53:59 +0300 Subject: [PATCH 10/67] [Discover] fix remaining tests --- .../__snapshots__/oss_features.test.ts.snap | 40 +++++++++++++++++-- .../expression/search_source_expression.scss | 2 +- .../builtin_alert_types/es_query/alert.ts | 9 +++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 85eb1f5b71e71..92e9394d6cddb 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -660,11 +660,15 @@ Array [ "privilege": Object { "alerting": Object { "alert": Object { - "all": Array [], + "all": Array [ + ".es-query", + ], "read": Array [], }, "rule": Object { - "all": Array [], + "all": Array [ + ".es-query", + ], "read": Array [], }, }, @@ -711,6 +715,18 @@ Array [ }, Object { "privilege": Object { + "alerting": Object { + "alert": Object { + "all": Array [ + ".es-query", + ], + }, + "rule": Object { + "all": Array [ + ".es-query", + ], + }, + }, "app": Array [ "discover", "kibana", @@ -1140,11 +1156,15 @@ Array [ "privilege": Object { "alerting": Object { "alert": Object { - "all": Array [], + "all": Array [ + ".es-query", + ], "read": Array [], }, "rule": Object { - "all": Array [], + "all": Array [ + ".es-query", + ], "read": Array [], }, }, @@ -1191,6 +1211,18 @@ Array [ }, Object { "privilege": Object { + "alerting": Object { + "alert": Object { + "all": Array [ + ".es-query", + ], + }, + "rule": Object { + "all": Array [ + ".es-query", + ], + }, + }, "app": Array [ "discover", "kibana", diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss index 28401e6657e87..418449eb792c1 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.scss @@ -1,6 +1,6 @@ .searchSourceAlertFilters { .euiExpression__value { - width: 80%; + width: 80%; } } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 2a2fdbcf9e4d2..21c9774a02ac2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -71,6 +71,7 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '<', threshold: [0], + searchType: 'esQuery', }); await createAlert({ @@ -79,6 +80,7 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '>', threshold: [-1], + searchType: 'esQuery', }); const docs = await waitForDocs(2); @@ -115,6 +117,7 @@ export default function alertTests({ getService }: FtrProviderContext) { thresholdComparator: '<', threshold: [0], timeField: 'date_epoch_millis', + searchType: 'esQuery', }); await createAlert({ @@ -124,6 +127,7 @@ export default function alertTests({ getService }: FtrProviderContext) { thresholdComparator: '>', threshold: [-1], timeField: 'date_epoch_millis', + searchType: 'esQuery', }); const docs = await waitForDocs(2); @@ -177,6 +181,7 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '<', threshold: [-1], + searchType: 'esQuery', }); await createAlert({ @@ -187,6 +192,7 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '>=', threshold: [0], + searchType: 'esQuery', }); const docs = await waitForDocs(1); @@ -211,6 +217,7 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '<', threshold: [1], + searchType: 'esQuery', }); const docs = await waitForDocs(1); @@ -263,6 +270,7 @@ export default function alertTests({ getService }: FtrProviderContext) { thresholdComparator: string; threshold: number[]; timeWindowSize?: number; + searchType: 'esQuery' | 'searchSource'; } async function createAlert(params: CreateAlertParams): Promise { @@ -308,6 +316,7 @@ export default function alertTests({ getService }: FtrProviderContext) { timeWindowUnit: 's', thresholdComparator: params.thresholdComparator, threshold: params.threshold, + searchType: params.searchType, }, }) .expect(200); From 3ae3857888773419078358d2b007ce660c3cdac3 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 24 Feb 2022 15:50:42 +0300 Subject: [PATCH 11/67] [Discover] add unit tests, add link back to stack management for es query --- src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/ui/index.ts | 2 +- .../components/top_nav/get_top_nav_links.tsx | 36 +- x-pack/plugins/alerting/server/mocks.ts | 7 + ....test.tsx => es_query_expression.test.tsx} | 41 +- .../es_query/expression/expression.tsx | 4 +- .../search_source_expression.test.tsx | 116 +++ .../expression/search_source_expression.tsx | 47 +- .../alert_types/es_query/validation.test.ts | 14 + .../public/alert_types/es_query/validation.ts | 2 +- .../alert_types/es_query/alert_type.test.ts | 762 ------------------ .../es_query/alert_type/alert_type.test.ts | 688 ++++++++++++++++ .../es_query/{ => alert_type}/alert_type.ts | 24 +- .../es_query_executor.ts | 15 +- .../{executor => alert_type}/index.ts | 5 +- .../search_source_executor.ts | 54 +- 16 files changed, 934 insertions(+), 885 deletions(-) rename x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/{expression.test.tsx => es_query_expression.test.tsx} (89%) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{ => alert_type}/alert_type.ts (89%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{executor => alert_type}/es_query_executor.ts (93%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{executor => alert_type}/index.ts (59%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{executor => alert_type}/search_source_executor.ts (66%) diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index ff74a15307234..a5d654aa90323 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -19,7 +19,7 @@ export * from './deprecated'; */ export { getEsQueryConfig } from '../common'; -export { FilterLabel, FilterItem, FilterBar } from './ui'; +export { FilterLabel, FilterItem } from './ui'; export { getDisplayValueFromFilter, generateFilters, diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 2cdf68ed5ac53..026db1b7c09ee 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -7,7 +7,7 @@ */ export type { IndexPatternSelectProps } from './index_pattern_select'; -export { FilterLabel, FilterItem, FilterBar } from './filter_bar'; +export { FilterLabel, FilterItem } from './filter_bar'; export type { QueryStringInputProps } from './query_string_input'; export { QueryStringInput } from './query_string_input'; export type { SearchBarProps, StatefulSearchBarProps } from './search_bar'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index 9d4b85341f7a0..3e1c20b336771 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; import type { DataView, ISearchSource } from 'src/plugins/data/common'; import { showOpenSearchPanel } from './show_open_search_panel'; @@ -19,7 +18,6 @@ import { GetStateReturn } from '../../services/discover_state'; import { openOptionsPopover } from './open_options_popover'; import type { TopNavMenuData } from '../../../../../../navigation/public'; import { openAlertsPopover } from './open_alerts_popover'; -import { toMountPoint, wrapWithTheme, MarkdownSimple } from '../../../../../../kibana_react/public'; /** * Helper function to build the top nav links @@ -70,30 +68,12 @@ export const getTopNavLinks = ({ defaultMessage: 'Alerts', }), run: (anchorElement: HTMLElement) => { - if (savedSearch.searchSource.getField('index')?.timeFieldName) { - openAlertsPopover({ - I18nContext: services.core.i18n.Context, - anchorElement, - searchSource: savedSearch.searchSource, - services, - }); - } else { - services.toastNotifications.addDanger({ - title: i18n.translate('discover.alert.errorHeader', { - defaultMessage: "Cant't create alert", - }), - text: toMountPoint( - wrapWithTheme( - - {i18n.translate('discover.alert.dataViewDoesNotHaveTimeFieldErrorMessage', { - defaultMessage: 'Data view does not have time field.', - })} - , - services.core.theme.theme$ - ) - ), - }); - } + openAlertsPopover({ + I18nContext: services.core.i18n.Context, + anchorElement, + searchSource: savedSearch.searchSource, + services, + }); }, testId: 'discoverAlertsButton', }; @@ -201,7 +181,9 @@ export const getTopNavLinks = ({ ...(services.capabilities.advancedSettings.save ? [options] : []), newSearch, openSearch, - ...(services.triggersActionsUi ? [alerts] : []), + ...(services.triggersActionsUi && savedSearch.searchSource.getField('index')?.timeFieldName + ? [alerts] + : []), shareSearch, inspectSearch, ...(services.capabilities.discover.save ? [saveSearch] : []), diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index afbc3ef9cec43..8f90760c7d2bb 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -11,7 +11,9 @@ import { Alert } from './alert'; import { elasticsearchServiceMock, savedObjectsClientMock, + httpServerMock, } from '../../../../src/core/server/mocks'; +import { dataPluginMock } from '../../../../src/plugins/data/server/mocks'; import { AlertInstanceContext, AlertInstanceState } from './types'; export { rulesClientMock }; @@ -95,6 +97,11 @@ const createAlertServicesMock = < shouldWriteAlerts: () => true, shouldStopExecution: () => true, search: createAbortableSearchServiceMock(), + searchSourceClient: Promise.resolve( + dataPluginMock + .createStartContract() + .search.searchSource.asScoped(httpServerMock.createKibanaRequest()) + ), }; }; export type AlertServicesMock = ReturnType; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx similarity index 89% rename from x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.test.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx index 03d9b0fe4e90d..5f1fd2fda071f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx @@ -10,7 +10,6 @@ import 'brace'; import { of } from 'rxjs'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import EsQueryAlertTypeExpression from '.'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { @@ -20,6 +19,7 @@ import { } from 'src/plugins/data/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { EsQueryAlertParams, SearchType } from '../types'; +import { EsQueryExpression } from './es_query_expression'; jest.mock('../../../../../../../src/plugins/kibana_react/public'); jest.mock('../../../../../../../src/plugins/es_ui_shared/public', () => ({ @@ -100,6 +100,18 @@ const createDataPluginMock = () => { const dataMock = createDataPluginMock(); const chartsStartMock = chartPluginMock.createStartContract(); +const defaultEsQueryExpressionParams: EsQueryAlertParams = { + size: 100, + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + index: ['test-index'], + timeField: '@timestamp', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + searchType: SearchType.esQuery, +}; + describe('EsQueryAlertTypeExpression', () => { beforeAll(() => { (useKibana as jest.Mock).mockReturnValue({ @@ -117,20 +129,6 @@ describe('EsQueryAlertTypeExpression', () => { }); }); - function getAlertParams(overrides = {}): EsQueryAlertParams { - return { - index: ['test-index'], - timeField: '@timestamp', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '>', - threshold: [0], - timeWindowSize: 15, - timeWindowUnit: 's', - searchType: SearchType.esQuery, - ...overrides, - }; - } async function setup(alertParams: EsQueryAlertParams) { const errors = { index: [], @@ -141,7 +139,7 @@ describe('EsQueryAlertTypeExpression', () => { }; const wrapper = mountWithIntl( - { } test('should render EsQueryAlertTypeExpression with expected components', async () => { - const wrapper = await setup(getAlertParams()); + const wrapper = await setup(defaultEsQueryExpressionParams); expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="queryJsonEditor"]').exists()).toBeTruthy(); @@ -182,7 +180,10 @@ describe('EsQueryAlertTypeExpression', () => { }); test('should render Test Query button disabled if alert params are invalid', async () => { - const wrapper = await setup(getAlertParams({ timeField: null })); + const wrapper = await setup({ + ...defaultEsQueryExpressionParams, + timeField: null, + } as unknown as EsQueryAlertParams); const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); expect(testQueryButton.exists()).toBeTruthy(); expect(testQueryButton.prop('disabled')).toBe(true); @@ -197,7 +198,7 @@ describe('EsQueryAlertTypeExpression', () => { }, }); dataMock.search.search.mockImplementation(() => searchResponseMock$); - const wrapper = await setup(getAlertParams()); + const wrapper = await setup(defaultEsQueryExpressionParams); const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); testQueryButton.simulate('click'); @@ -218,7 +219,7 @@ describe('EsQueryAlertTypeExpression', () => { dataMock.search.search.mockImplementation(() => { throw new Error('What is this query'); }); - const wrapper = await setup(getAlertParams()); + const wrapper = await setup(defaultEsQueryExpressionParams); const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); testQueryButton.simulate('click'); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx index fab6d935837dc..2195cc643122f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx @@ -13,7 +13,7 @@ import 'brace/theme/github'; import { EuiSpacer, EuiCallOut } from '@elastic/eui'; import { RuleTypeParamsExpressionProps } from '../../../../../triggers_actions_ui/public'; import { EsQueryAlertParams } from '../types'; -import { SearchSourceThresholdExpression } from './search_source_expression'; +import { SearchSourceExpression } from './search_source_expression'; import { EsQueryExpression } from './es_query_expression'; import { isSearchSourceAlert } from '../util'; @@ -61,7 +61,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< )} {isSearchSource ? ( - + ) : ( )} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx new file mode 100644 index 0000000000000..4c2ef2f96540b --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx @@ -0,0 +1,116 @@ +/* + * 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 { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { DataPublicPluginStart, ISearchStart } from 'src/plugins/data/public'; +import { EsQueryAlertParams, SearchType } from '../types'; +import { SearchSourceExpression } from './search_source_expression'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { act } from 'react-dom/test-utils'; +import { EuiCallOut, EuiLoadingSpinner } from '@elastic/eui'; +import { ReactWrapper } from 'enzyme'; + +const dataMock = dataPluginMock.createStartContract() as DataPublicPluginStart & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + search: ISearchStart & { searchSource: { create: jest.MockedFunction } }; +}; +const chartsStartMock = chartPluginMock.createStartContract(); + +const defaultSearchSourceExpressionParams: EsQueryAlertParams = { + size: 100, + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + index: ['test-index'], + timeField: '@timestamp', + searchType: SearchType.searchSource, + searchConfiguration: {}, +}; + +const searchSourceMock = { + getField: (name: string) => { + if (name === 'filter') { + return []; + } + return ''; + }, +}; + +const setup = async (alertParams: EsQueryAlertParams) => { + const errors = { + size: [], + timeField: [], + timeWindowSize: [], + searchConfiguration: [], + }; + + const wrapper = mountWithIntl( + {}} + setRuleProperty={() => {}} + errors={errors} + data={dataMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + /> + ); + + return wrapper; +}; + +const rerender = async (wrapper: ReactWrapper) => { + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + await update(); +}; + +describe('SearchSourceAlertTypeExpression', () => { + test('should render loading prompt', async () => { + dataMock.search.searchSource.create.mockImplementation(() => + Promise.resolve(() => searchSourceMock) + ); + + const wrapper = await setup(defaultSearchSourceExpressionParams); + + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + }); + + test('should render error prompt', async () => { + dataMock.search.searchSource.create.mockImplementation(() => + Promise.reject(() => 'test error') + ); + + const wrapper = await setup(defaultSearchSourceExpressionParams); + rerender(wrapper); + + expect(wrapper.find(EuiCallOut).exists()).toBeTruthy(); + }); + + test('should render SearchSourceAlertTypeExpression with expected components', async () => { + dataMock.search.searchSource.create.mockImplementation(() => + Promise.resolve(() => searchSourceMock) + ); + + const wrapper = await setup(defaultSearchSourceExpressionParams); + rerender(wrapper); + + expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 2d924518f7c62..af4c72cc0c705 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -15,6 +15,7 @@ import { EuiText, EuiLoadingSpinner, EuiEmptyPrompt, + EuiCallOut, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Filter, ISearchSource } from '../../../../../../../src/plugins/data/common'; @@ -28,7 +29,7 @@ import { import { DEFAULT_VALUES } from '../constants'; import { ReadOnlyFilterItems } from './read_only_filter_items'; -export const SearchSourceThresholdExpression = ({ +export const SearchSourceExpression = ({ ruleParams, setRuleParams, setRuleProperty, @@ -44,6 +45,7 @@ export const SearchSourceThresholdExpression = ({ size, } = ruleParams; const [usedSearchSource, setUsedSearchSource] = useState(); + const [paramsError, setParamsError] = useState(); const [currentAlertParams, setCurrentAlertParams] = useState< EsQueryAlertParams @@ -68,37 +70,44 @@ export const SearchSourceThresholdExpression = ({ [setRuleParams] ); - useEffect(() => { - setRuleProperty('params', currentAlertParams); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => setRuleProperty('params', currentAlertParams), []); useEffect(() => { async function initSearchSource() { - const loadedSearchSource = await data.search.searchSource.create(searchConfiguration); - setUsedSearchSource(loadedSearchSource); + try { + const loadedSearchSource = await data.search.searchSource.create(searchConfiguration); + setUsedSearchSource(loadedSearchSource); + } catch (error) { + setParamsError(error); + } } if (searchConfiguration) { initSearchSource(); } }, [data.search.searchSource, searchConfiguration]); - if (!usedSearchSource) { + if (paramsError) { return ( - } - body={ - - - - } - /> + <> + +

{paramsError.message}

+
+ + ); } + if (!usedSearchSource) { + return } />; + } + const dataView = usedSearchSource.getField('index')!; const query = usedSearchSource.getField('query')!; const filters = (usedSearchSource.getField('filter') as Filter[]).filter( diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts index a3f5e0fce1942..5b0f76042abb2 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts @@ -69,6 +69,20 @@ describe('expression params validation', () => { expect(validateExpression(initialParams).errors.esQuery[0]).toBe(`Query field is required.`); }); + test('if searchConfiguration property is not set should return proper error message', () => { + const initialParams = { + size: 100, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + searchType: SearchType.searchSource, + } as EsQueryAlertParams; + expect(validateExpression(initialParams).errors.searchConfiguration.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.searchConfiguration[0]).toBe( + `Search source configuration is required.` + ); + }); + test('if threshold0 property is not set should return proper error message', () => { const initialParams: EsQueryAlertParams = { index: ['test'], diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts index 1e23aba703527..4766ce68dba4a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts @@ -83,7 +83,7 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR const isSearchSource = isSearchSourceAlert(alertParams); if (isSearchSource) { if (!alertParams.searchConfiguration) { - errors.index.push( + errors.searchConfiguration.push( i18n.translate( 'xpack.stackAlerts.esQuery.ui.validation.error.requiredSearchConfiguration', { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts deleted file mode 100644 index 3d38c1ba591b3..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ /dev/null @@ -1,762 +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 uuid from 'uuid'; -import type { Writable } from '@kbn/utility-types'; -import { AlertServices } from '../../../../alerting/server'; -import { - AlertServicesMock, - alertsMock, - AlertInstanceMock, -} from '../../../../alerting/server/mocks'; -import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; -import { getAlertType } from './alert_type'; -import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; -import { ActionContext } from './action_context'; -import { ESSearchResponse, ESSearchRequest } from '../../../../../../src/core/types/elasticsearch'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; -import { coreMock } from '../../../../../../src/core/server/mocks'; -import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; -import { OnlyEsQueryAlertParams } from './types'; - -describe('alertType', () => { - const logger = loggingSystemMock.create().get(); - const coreSetup = coreMock.createSetup(); - const alertType = getAlertType(logger, coreSetup); - - it('alert type creation structure is the expected value', async () => { - expect(alertType.id).toBe('.es-query'); - expect(alertType.name).toBe('Elasticsearch query'); - expect(alertType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]); - - expect(alertType.actionVariables).toMatchInlineSnapshot(` - Object { - "context": Array [ - Object { - "description": "A message for the alert.", - "name": "message", - }, - Object { - "description": "A title for the alert.", - "name": "title", - }, - Object { - "description": "The date that the alert met the threshold condition.", - "name": "date", - }, - Object { - "description": "The value that met the threshold condition.", - "name": "value", - }, - Object { - "description": "The documents that met the threshold condition.", - "name": "hits", - }, - Object { - "description": "A string that describes the threshold condition.", - "name": "conditions", - }, - Object { - "description": "A link to see records that triggered this alert.", - "name": "link", - }, - ], - "params": Array [ - Object { - "description": "The number of hits to retrieve for each query.", - "name": "size", - }, - Object { - "description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", - "name": "threshold", - }, - Object { - "description": "A function to determine if the threshold has been met.", - "name": "thresholdComparator", - }, - Object { - "description": "The type of search is used.", - "name": "searchType", - }, - Object { - "description": "Serialized search source fields used to fetch the documents from Elasticsearch.", - "name": "searchConfiguration", - }, - Object { - "description": "The string representation of the Elasticsearch query.", - "name": "esQuery", - }, - Object { - "description": "The index the query was run against.", - "name": "index", - }, - ], - } - `); - }); - - it('validator succeeds with valid params', async () => { - const params: Partial> = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: '<', - threshold: [0], - searchType: 'esQuery', - }; - - expect(alertType.validate?.params?.validate(params)).toBeTruthy(); - }); - - it('validator fails with invalid params - threshold', async () => { - const paramsSchema = alertType.validate?.params; - if (!paramsSchema) throw new Error('params validator not set'); - - const params: Partial> = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: 'between', - threshold: [0], - searchType: 'esQuery', - }; - - expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( - `"[threshold]: must have two elements for the \\"between\\" comparator"` - ); - }); - - it('alert executor handles no documentes returned by ES', async () => { - const params: OnlyEsQueryAlertParams = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: 'between', - threshold: [0], - searchType: 'esQuery', - }; - const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - - const searchResult: ESSearchResponse = generateResults([]); - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) - ); - - const result = await alertType.executor({ - alertId: uuid.v4(), - executionId: uuid.v4(), - startedAt: new Date(), - previousStartedAt: new Date(), - services: alertServices as unknown as AlertServices< - EsQueryAlertState, - ActionContext, - typeof ActionGroupId - >, - params: params as EsQueryAlertParams, - state: { - latestTimestamp: undefined, - }, - spaceId: uuid.v4(), - name: uuid.v4(), - tags: [], - createdBy: null, - updatedBy: null, - rule: { - name: uuid.v4(), - tags: [], - consumer: '', - producer: '', - ruleTypeId: '', - ruleTypeName: '', - enabled: true, - schedule: { - interval: '1h', - }, - actions: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - throttle: null, - notifyWhen: null, - }, - }); - - expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); - - expect(result).toMatchInlineSnapshot(` - Object { - "latestTimestamp": undefined, - } - `); - }); - - it('alert executor returns the latestTimestamp of the newest detected document', async () => { - const params: OnlyEsQueryAlertParams = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: '>', - threshold: [0], - searchType: 'esQuery', - }; - const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - - const newestDocumentTimestamp = Date.now(); - - const searchResult: ESSearchResponse = generateResults([ - { - 'time-field': newestDocumentTimestamp, - }, - { - 'time-field': newestDocumentTimestamp - 1000, - }, - { - 'time-field': newestDocumentTimestamp - 2000, - }, - ]); - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) - ); - - const result = await alertType.executor({ - alertId: uuid.v4(), - executionId: uuid.v4(), - startedAt: new Date(), - previousStartedAt: new Date(), - services: alertServices as unknown as AlertServices< - EsQueryAlertState, - ActionContext, - typeof ActionGroupId - >, - params: params as EsQueryAlertParams, - state: { - latestTimestamp: undefined, - }, - spaceId: uuid.v4(), - name: uuid.v4(), - tags: [], - createdBy: null, - updatedBy: null, - rule: { - name: uuid.v4(), - tags: [], - consumer: '', - producer: '', - ruleTypeId: '', - ruleTypeName: '', - enabled: true, - schedule: { - interval: '1h', - }, - actions: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - throttle: null, - notifyWhen: null, - }, - }); - - expect(alertServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); - - expect(result).toMatchObject({ - latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), - }); - }); - - it('alert executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => { - const params: OnlyEsQueryAlertParams = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: '>', - threshold: [0], - searchType: 'esQuery', - }; - const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - - const previousTimestamp = Date.now(); - const newestDocumentTimestamp = previousTimestamp + 1000; - - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - generateResults([ - { - 'time-field': newestDocumentTimestamp, - }, - ]) - ) - ); - - const executorOptions = { - alertId: uuid.v4(), - startedAt: new Date(), - previousStartedAt: new Date(), - services: alertServices as unknown as AlertServices< - EsQueryAlertState, - ActionContext, - typeof ActionGroupId - >, - params, - spaceId: uuid.v4(), - name: uuid.v4(), - tags: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - consumer: '', - throttle: null, - notifyWhen: null, - schedule: { - interval: '1h', - }, - }; - const result = await alertType.executor({ - ...executorOptions, - state: { - // @ts-expect-error previousTimestamp is numeric, but should be string (this was a bug prior to v7.12.1) - latestTimestamp: previousTimestamp, - }, - }); - - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward - latestTimestamp: new Date(previousTimestamp).toISOString(), - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); - - expect(result).toMatchObject({ - latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), - }); - }); - - it('alert executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => { - const params: OnlyEsQueryAlertParams = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: '>', - threshold: [0], - searchType: 'esQuery', - }; - const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - - const oldestDocumentTimestamp = Date.now(); - - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - generateResults([ - { - 'time-field': oldestDocumentTimestamp, - }, - { - 'time-field': oldestDocumentTimestamp - 1000, - }, - ]) - ) - ); - - const result = await alertType.executor({ - alertId: uuid.v4(), - executionId: uuid.v4(), - startedAt: new Date(), - previousStartedAt: new Date(), - services: alertServices as unknown as AlertServices< - EsQueryAlertState, - ActionContext, - typeof ActionGroupId - >, - params: params as EsQueryAlertParams, - state: { - // inaalid legacy `latestTimestamp` - latestTimestamp: 'FaslK3QBySSL_rrj9zM5', - }, - spaceId: uuid.v4(), - name: uuid.v4(), - tags: [], - createdBy: null, - updatedBy: null, - rule: { - name: uuid.v4(), - tags: [], - consumer: '', - producer: '', - ruleTypeId: '', - ruleTypeName: '', - enabled: true, - schedule: { - interval: '1h', - }, - actions: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - throttle: null, - notifyWhen: null, - }, - }); - - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); - - expect(result).toMatchObject({ - latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), - }); - }); - - it('alert executor carries over the queried latestTimestamp in the alert state', async () => { - const params: OnlyEsQueryAlertParams = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: '>', - threshold: [0], - searchType: 'esQuery', - }; - const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - - const oldestDocumentTimestamp = Date.now(); - - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - generateResults([ - { - 'time-field': oldestDocumentTimestamp, - }, - ]) - ) - ); - - const executorOptions = { - alertId: uuid.v4(), - executionId: uuid.v4(), - startedAt: new Date(), - previousStartedAt: new Date(), - services: alertServices as unknown as AlertServices< - EsQueryAlertState, - ActionContext, - typeof ActionGroupId - >, - params: params as EsQueryAlertParams, - state: { - latestTimestamp: undefined, - }, - spaceId: uuid.v4(), - name: uuid.v4(), - tags: [], - createdBy: null, - updatedBy: null, - rule: { - name: uuid.v4(), - tags: [], - consumer: '', - producer: '', - ruleTypeId: '', - ruleTypeName: '', - enabled: true, - schedule: { - interval: '1h', - }, - actions: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - throttle: null, - notifyWhen: null, - }, - }; - const result = await alertType.executor(executorOptions); - - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); - - expect(result).toMatchObject({ - latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), - }); - - const newestDocumentTimestamp = oldestDocumentTimestamp + 5000; - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - generateResults([ - { - 'time-field': newestDocumentTimestamp, - }, - { - 'time-field': newestDocumentTimestamp - 1000, - }, - ]) - ) - ); - - const secondResult = await alertType.executor({ - ...executorOptions, - state: result as EsQueryAlertState, - }); - const existingInstance: AlertInstanceMock = - alertServices.alertFactory.create.mock.results[1].value; - expect(existingInstance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); - - expect(secondResult).toMatchObject({ - latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), - }); - }); - - it('alert executor ignores tie breaker sort values', async () => { - const params: OnlyEsQueryAlertParams = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: '>', - threshold: [0], - searchType: 'esQuery', - }; - const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - - const oldestDocumentTimestamp = Date.now(); - - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - generateResults( - [ - { - 'time-field': oldestDocumentTimestamp, - }, - { - 'time-field': oldestDocumentTimestamp - 1000, - }, - ], - true - ) - ) - ); - - const result = await alertType.executor({ - alertId: uuid.v4(), - executionId: uuid.v4(), - startedAt: new Date(), - previousStartedAt: new Date(), - services: alertServices as unknown as AlertServices< - EsQueryAlertState, - ActionContext, - typeof ActionGroupId - >, - params: params as EsQueryAlertParams, - state: { - latestTimestamp: undefined, - }, - spaceId: uuid.v4(), - name: uuid.v4(), - tags: [], - createdBy: null, - updatedBy: null, - rule: { - name: uuid.v4(), - tags: [], - consumer: '', - producer: '', - ruleTypeId: '', - ruleTypeName: '', - enabled: true, - schedule: { - interval: '1h', - }, - actions: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - throttle: null, - notifyWhen: null, - }, - }); - - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); - - expect(result).toMatchObject({ - latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), - }); - }); - - it('alert executor ignores results with no sort values', async () => { - const params: OnlyEsQueryAlertParams = { - index: ['index-name'], - timeField: 'time-field', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - timeWindowSize: 5, - timeWindowUnit: 'm', - thresholdComparator: '>', - threshold: [0], - searchType: 'esQuery', - }; - const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - - const oldestDocumentTimestamp = Date.now(); - - alertServices.search.asCurrentUser.search.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - generateResults( - [ - { - 'time-field': oldestDocumentTimestamp, - }, - { - 'time-field': oldestDocumentTimestamp - 1000, - }, - ], - true, - true - ) - ) - ); - - const result = await alertType.executor({ - alertId: uuid.v4(), - executionId: uuid.v4(), - startedAt: new Date(), - previousStartedAt: new Date(), - services: alertServices as unknown as AlertServices< - EsQueryAlertState, - ActionContext, - typeof ActionGroupId - >, - params: params as EsQueryAlertParams, - state: { - latestTimestamp: undefined, - }, - spaceId: uuid.v4(), - name: uuid.v4(), - tags: [], - createdBy: null, - updatedBy: null, - rule: { - name: uuid.v4(), - tags: [], - consumer: '', - producer: '', - ruleTypeId: '', - ruleTypeName: '', - enabled: true, - schedule: { - interval: '1h', - }, - actions: [], - createdBy: null, - updatedBy: null, - createdAt: new Date(), - updatedAt: new Date(), - throttle: null, - notifyWhen: null, - }, - }); - - const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; - expect(instance.replaceState).toHaveBeenCalledWith({ - latestTimestamp: undefined, - dateStart: expect.any(String), - dateEnd: expect.any(String), - }); - - expect(result).toMatchObject({ - latestTimestamp: new Date(oldestDocumentTimestamp - 1000).toISOString(), - }); - }); -}); - -function generateResults( - docs: Array<{ 'time-field': unknown; [key: string]: unknown }>, - includeTieBreaker: boolean = false, - skipSortOnFirst: boolean = false -): ESSearchResponse { - const hits = docs.map((doc, index) => ({ - _index: 'foo', - _type: '_doc', - _id: `${index}`, - _score: 0, - ...(skipSortOnFirst && index === 0 - ? {} - : { - sort: (includeTieBreaker - ? ['FaslK3QBySSL_rrj9zM5', doc['time-field']] - : [doc['time-field']]) as string[], - }), - _source: doc, - })); - return { - took: 10, - timed_out: false, - _shards: { - total: 10, - successful: 10, - failed: 0, - skipped: 0, - }, - hits: { - total: { - value: docs.length, - relation: 'eq', - }, - max_score: 100, - hits, - }, - }; -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts new file mode 100644 index 0000000000000..d119be34bdcd5 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts @@ -0,0 +1,688 @@ +/* + * 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 uuid from 'uuid'; +import type { Writable } from '@kbn/utility-types'; +import { AlertServices } from '../../../../../alerting/server'; +import { + AlertServicesMock, + alertsMock, + AlertInstanceMock, +} from '../../../../../alerting/server/mocks'; +import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; +import { getAlertType } from './alert_type'; +import { EsQueryAlertParams, EsQueryAlertState } from '../alert_type_params'; +import { ActionContext } from '../action_context'; +import { + ESSearchResponse, + ESSearchRequest, +} from '../../../../../../../src/core/types/elasticsearch'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; +import { coreMock } from '../../../../../../../src/core/server/mocks'; +import { ActionGroupId, ConditionMetAlertInstanceId } from '../constants'; +import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from '../types'; +import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks'; + +const logger = loggingSystemMock.create().get(); +const coreSetup = coreMock.createSetup(); +const alertType = getAlertType(logger, coreSetup); + +describe('alertType', () => { + it('alert type creation structure is the expected value', async () => { + expect(alertType.id).toBe('.es-query'); + expect(alertType.name).toBe('Elasticsearch query'); + expect(alertType.actionGroups).toEqual([{ id: 'query matched', name: 'Query matched' }]); + + expect(alertType.actionVariables).toMatchInlineSnapshot(` + Object { + "context": Array [ + Object { + "description": "A message for the alert.", + "name": "message", + }, + Object { + "description": "A title for the alert.", + "name": "title", + }, + Object { + "description": "The date that the alert met the threshold condition.", + "name": "date", + }, + Object { + "description": "The value that met the threshold condition.", + "name": "value", + }, + Object { + "description": "The documents that met the threshold condition.", + "name": "hits", + }, + Object { + "description": "A string that describes the threshold condition.", + "name": "conditions", + }, + Object { + "description": "A link to see records that triggered this alert if it created from discover. + In case of Elastic query alert, this link will navigate to stack management.", + "name": "link", + }, + ], + "params": Array [ + Object { + "description": "The number of hits to retrieve for each query.", + "name": "size", + }, + Object { + "description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", + "name": "threshold", + }, + Object { + "description": "A function to determine if the threshold has been met.", + "name": "thresholdComparator", + }, + Object { + "description": "The type of search is used.", + "name": "searchType", + }, + Object { + "description": "Serialized search source fields used to fetch the documents from Elasticsearch.", + "name": "searchConfiguration", + }, + Object { + "description": "The string representation of the Elasticsearch query.", + "name": "esQuery", + }, + Object { + "description": "The index the query was run against.", + "name": "index", + }, + ], + } + `); + }); + + describe('elasticsearch query', () => { + it('validator succeeds with valid es query params', async () => { + const params: Partial> = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '<', + threshold: [0], + searchType: 'esQuery', + }; + + expect(alertType.validate?.params?.validate(params)).toBeTruthy(); + }); + + it('validator fails with invalid es query params - threshold', async () => { + const paramsSchema = alertType.validate?.params; + if (!paramsSchema) throw new Error('params validator not set'); + + const params: Partial> = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: 'between', + threshold: [0], + searchType: 'esQuery', + }; + + expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( + `"[threshold]: must have two elements for the \\"between\\" comparator"` + ); + }); + + it('alert executor handles no documents returned by ES', async () => { + const params: OnlyEsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: 'between', + threshold: [0], + searchType: 'esQuery', + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const searchResult: ESSearchResponse = generateResults([]); + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) + ); + + const result = await invokeExecutor({ params, alertServices }); + + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); + + expect(result).toMatchInlineSnapshot(` + Object { + "latestTimestamp": undefined, + } + `); + }); + + it('alert executor returns the latestTimestamp of the newest detected document', async () => { + const params: OnlyEsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + searchType: 'esQuery', + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const newestDocumentTimestamp = Date.now(); + + const searchResult: ESSearchResponse = generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + { + 'time-field': newestDocumentTimestamp - 1000, + }, + { + 'time-field': newestDocumentTimestamp - 2000, + }, + ]); + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(searchResult) + ); + + const result = await invokeExecutor({ params, alertServices }); + + expect(alertServices.alertFactory.create).toHaveBeenCalledWith(ConditionMetAlertInstanceId); + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor correctly handles numeric time fields that were stored by legacy rules prior to v7.12.1', async () => { + const params: OnlyEsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + searchType: 'esQuery', + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const previousTimestamp = Date.now(); + const newestDocumentTimestamp = previousTimestamp + 1000; + + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + ]) + ) + ); + + const result = await invokeExecutor({ + params, + alertServices, + state: { + // @ts-expect-error previousTimestamp is numeric, but should be string (this was a bug prior to v7.12.1) + latestTimestamp: previousTimestamp, + }, + }); + + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + // ensure the invalid "latestTimestamp" in the state is stored as an ISO string going forward + latestTimestamp: new Date(previousTimestamp).toISOString(), + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor ignores previous invalid latestTimestamp values stored by legacy rules prior to v7.12.1', async () => { + const params: OnlyEsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + searchType: 'esQuery', + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ]) + ) + ); + + const result = await invokeExecutor({ params, alertServices }); + + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor carries over the queried latestTimestamp in the alert state', async () => { + const params: OnlyEsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + searchType: 'esQuery', + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': oldestDocumentTimestamp, + }, + ]) + ) + ); + + const result = await invokeExecutor({ params, alertServices }); + + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + }); + + const newestDocumentTimestamp = oldestDocumentTimestamp + 5000; + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults([ + { + 'time-field': newestDocumentTimestamp, + }, + { + 'time-field': newestDocumentTimestamp - 1000, + }, + ]) + ) + ); + + const secondResult = await invokeExecutor({ + params, + alertServices, + state: result as EsQueryAlertState, + }); + + const existingInstance: AlertInstanceMock = + alertServices.alertFactory.create.mock.results[1].value; + expect(existingInstance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(secondResult).toMatchObject({ + latestTimestamp: new Date(newestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor ignores tie breaker sort values', async () => { + const params: OnlyEsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + searchType: 'esQuery', + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults( + [ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ], + true + ) + ) + ); + + const result = await invokeExecutor({ params, alertServices }); + + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp).toISOString(), + }); + }); + + it('alert executor ignores results with no sort values', async () => { + const params: OnlyEsQueryAlertParams = { + index: ['index-name'], + timeField: 'time-field', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '>', + threshold: [0], + searchType: 'esQuery', + }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const oldestDocumentTimestamp = Date.now(); + + alertServices.search.asCurrentUser.search.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + generateResults( + [ + { + 'time-field': oldestDocumentTimestamp, + }, + { + 'time-field': oldestDocumentTimestamp - 1000, + }, + ], + true, + true + ) + ) + ); + + const result = await invokeExecutor({ params, alertServices }); + + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(instance.replaceState).toHaveBeenCalledWith({ + latestTimestamp: undefined, + dateStart: expect.any(String), + dateEnd: expect.any(String), + }); + + expect(result).toMatchObject({ + latestTimestamp: new Date(oldestDocumentTimestamp - 1000).toISOString(), + }); + }); + }); + + describe('search source query', () => { + const dataViewMock = { + id: 'test-id', + title: 'test-title', + timeFieldName: 'time-field', + fields: [ + { + name: 'message', + type: 'string', + displayName: 'message', + scripted: false, + filterable: false, + aggregatable: false, + }, + { + name: 'timestamp', + type: 'date', + displayName: 'timestamp', + scripted: false, + filterable: false, + aggregatable: false, + }, + ], + }; + const defaultParams: OnlySearchSourceAlertParams = { + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '<', + threshold: [0], + searchConfiguration: {}, + searchType: 'searchSource', + }; + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('validator succeeds with valid search source params', async () => { + const params = defaultParams; + + expect(alertType.validate?.params?.validate(params)).toBeTruthy(); + }); + + it('validator fails with invalid search source params - esQuery provided', async () => { + const paramsSchema = alertType.validate?.params!; + const params: Partial> = { + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '<', + threshold: [0], + esQuery: '', + searchType: 'searchSource', + }; + + expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( + `"[esQuery]: a value wasn't expected to be present"` + ); + }); + + it('alert executor handles no documents returned by ES', async () => { + const params = defaultParams; + const searchResult: ESSearchResponse = generateResults([]); + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { + if (name === 'index') { + return dataViewMock; + } + }); + (searchSourceInstanceMock.fetch as jest.Mock).mockResolvedValueOnce(searchResult); + + await invokeExecutor({ params, alertServices }); + + expect(alertServices.alertFactory.create).not.toHaveBeenCalled(); + }); + + it('alert executor throws an error when index does not have time field', async () => { + const params = defaultParams; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { + if (name === 'index') { + return { dataViewMock, timeFieldName: undefined }; + } + }); + + await expect(invokeExecutor({ params, alertServices })).rejects.toThrow( + 'Invalid data view without timeFieldName.' + ); + }); + + it('alert executor schedule actions when condition met', async () => { + const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + const alertServices: AlertServicesMock = alertsMock.createAlertServices(); + + const scheduleActionsMock = jest.fn(); + alertServices.alertFactory.create.mockImplementationOnce(() => ({ + scheduleActions: scheduleActionsMock, + })); + + (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { + if (name === 'index') { + return dataViewMock; + } + }); + + (searchSourceInstanceMock.fetch as jest.Mock).mockResolvedValueOnce({ + hits: { total: 3, hits: [{}, {}, {}] }, + }); + + await invokeExecutor({ params, alertServices }); + + const instance: AlertInstanceMock = alertServices.alertFactory.create.mock.results[0].value; + expect(instance.scheduleActions).toHaveBeenCalled(); + }); + }); +}); + +function generateResults( + docs: Array<{ 'time-field': unknown; [key: string]: unknown }>, + includeTieBreaker: boolean = false, + skipSortOnFirst: boolean = false +): ESSearchResponse { + const hits = docs.map((doc, index) => ({ + _index: 'foo', + _type: '_doc', + _id: `${index}`, + _score: 0, + ...(skipSortOnFirst && index === 0 + ? {} + : { + sort: (includeTieBreaker + ? ['FaslK3QBySSL_rrj9zM5', doc['time-field']] + : [doc['time-field']]) as string[], + }), + _source: doc, + })); + return { + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total: { + value: docs.length, + relation: 'eq', + }, + max_score: 100, + hits, + }, + }; +} + +async function invokeExecutor({ + params, + alertServices, + state, +}: { + params: OnlySearchSourceAlertParams | OnlyEsQueryAlertParams; + alertServices: AlertServicesMock; + state?: EsQueryAlertState; +}) { + return await alertType.executor({ + alertId: uuid.v4(), + executionId: uuid.v4(), + startedAt: new Date(), + previousStartedAt: new Date(), + services: alertServices as unknown as AlertServices< + EsQueryAlertState, + ActionContext, + typeof ActionGroupId + >, + params: params as EsQueryAlertParams, + state: { + latestTimestamp: undefined, + ...state, + }, + spaceId: uuid.v4(), + name: uuid.v4(), + tags: [], + createdBy: null, + updatedBy: null, + rule: { + name: uuid.v4(), + tags: [], + consumer: '', + producer: '', + ruleTypeId: '', + ruleTypeName: '', + enabled: true, + schedule: { + interval: '1h', + }, + actions: [], + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + throttle: null, + notifyWhen: null, + }, + }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts similarity index 89% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index 9e2f1274b5ba1..acc522998051b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -7,17 +7,18 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger } from 'src/core/server'; -import { RuleType } from '../../types'; -import { ActionContext } from './action_context'; +import { RuleType } from '../../../types'; +import { ActionContext } from '../action_context'; import { EsQueryAlertParams, EsQueryAlertParamsSchema, EsQueryAlertState, -} from './alert_type_params'; -import { STACK_ALERTS_FEATURE_ID } from '../../../common'; -import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; -import { ActionGroupId, ES_QUERY_ID } from './constants'; -import { esQueryExecutor, searchSourceExecutor } from './executor'; +} from '../alert_type_params'; +import { STACK_ALERTS_FEATURE_ID } from '../../../../common'; +import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from '../types'; +import { ActionGroupId, ES_QUERY_ID } from '../constants'; +import { esQueryExecutor } from './es_query_executor'; +import { searchSourceExecutor } from './search_source_executor'; export function getAlertType( logger: Logger, @@ -134,7 +135,8 @@ export function getAlertType( const actionVariableContextLinkLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', { - defaultMessage: 'A link to see records that triggered this alert.', + defaultMessage: `A link to see records that triggered this alert if it created from discover. + In case of Elastic query alert, this link will navigate to stack management.`, } ); @@ -174,7 +176,11 @@ export function getAlertType( async function executor(options: ExecutorOptions) { if (isEsQueryAlert(options)) { - return await esQueryExecutor(logger, options as ExecutorOptions); + return await esQueryExecutor( + logger, + core, + options as ExecutorOptions + ); } else { return await searchSourceExecutor( logger, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/es_query_executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/es_query_executor.ts similarity index 93% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/es_query_executor.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/es_query_executor.ts index 51c55fbcf1db4..e6cb2dcee8423 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/es_query_executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/es_query_executor.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Logger } from 'src/core/server'; +import { CoreSetup, Logger } from 'src/core/server'; import { EsQueryAlertActionContext, addMessages } from '../action_context'; import { ComparatorFns, getHumanReadableComparator } from '../../lib'; import { parseDuration } from '../../../../../alerting/server'; @@ -17,11 +17,13 @@ import { ActionGroupId, ConditionMetAlertInstanceId, ES_QUERY_ID } from '../cons export async function esQueryExecutor( logger: Logger, + core: CoreSetup, options: ExecutorOptions ) { const { alertId, name, services, params, state } = options; const { alertFactory, search } = services; const previousTimestamp = state.latestTimestamp; + const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; const abortableEsClient = search.asCurrentUser; const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); @@ -82,12 +84,14 @@ export async function esQueryExecutor( track_total_hits: true, }); - logger.debug(`alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}`); + logger.debug( + `es query alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}` + ); const { body: searchResult } = await abortableEsClient.search(query); logger.debug( - `alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` + ` es query alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` ); const numMatches = (searchResult.hits.total as estypes.SearchTotalHits).value; @@ -112,6 +116,7 @@ export async function esQueryExecutor( value: numMatches, conditions: humanFn, hits: searchResult.hits.hits, + link: `${publicBaseUrl}/app/management/insightsAndAlerting/triggersActions/rule/${alertId}`, }; const actionContext = addMessages(options, baseContext, params); @@ -130,9 +135,7 @@ export async function esQueryExecutor( } } - return { - latestTimestamp: timestamp, - }; + return { latestTimestamp: timestamp }; } function getInvalidComparatorError(comparator: string) { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/index.ts similarity index 59% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/index.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/index.ts index 67740338a6ef4..d670d9cb7c566 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/index.ts @@ -5,7 +5,4 @@ * 2.0. */ -import { esQueryExecutor } from './es_query_executor'; -import { searchSourceExecutor } from './search_source_executor'; - -export { esQueryExecutor, searchSourceExecutor }; +export { getAlertType } from './alert_type'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/search_source_executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/search_source_executor.ts similarity index 66% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/search_source_executor.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/search_source_executor.ts index 3e17834a2125f..201d24e50061f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor/search_source_executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/search_source_executor.ts @@ -19,14 +19,10 @@ export async function searchSourceExecutor( core: CoreSetup, options: ExecutorOptions ) { - const { name, params, alertId, state, services } = options; + const { name, params, alertId, services } = options; const timestamp = new Date().toISOString(); const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; - logger.debug( - `searchThreshold (${alertId}) previousTimestamp: ${state.previousTimestamp}, previousTimeRange ${state.previousTimeRange}` - ); - const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { throw new Error( @@ -45,7 +41,7 @@ export async function searchSourceExecutor( const timeFieldName = index?.timeFieldName; if (!timeFieldName) { - throw new Error('Invalid data view without timeFieldName'); + throw new Error('Invalid data view without timeFieldName.'); } loadedSearchSource.setField('size', params.size); @@ -59,45 +55,37 @@ export async function searchSourceExecutor( const searchSourceChild = loadedSearchSource.createChild(); searchSourceChild.setField('filter', filter); - let nrOfDocs = 0; - let searchResult; - try { - logger.info( - `searchThreshold (${alertId}) query: ${JSON.stringify( - searchSourceChild.getSearchRequestBody() - )}` - ); - searchResult = await searchSourceChild.fetch(); - nrOfDocs = Number(searchResult.hits.total); - logger.info(`searchThreshold (${alertId}) nrOfDocs: ${nrOfDocs}`); - } catch (error) { - logger.error('Error fetching documents: ' + error.message); - throw error; - } + logger.debug( + `search source query alert (${alertId}) query: ${JSON.stringify( + searchSourceChild.getSearchRequestBody() + )}` + ); + + const searchResult = await searchSourceChild.fetch(); + const matchedDocsNumber = Number(searchResult.hits.total); - const met = compareFn(nrOfDocs, params.threshold); + logger.debug( + `search source query alert (${alertId}) number of matched documents: ${matchedDocsNumber}` + ); + const met = compareFn(matchedDocsNumber, params.threshold); if (met) { - const conditions = `${nrOfDocs} is ${getHumanReadableComparator(params.thresholdComparator)} ${ - params.threshold - }`; + const conditions = `${matchedDocsNumber} is ${getHumanReadableComparator( + params.thresholdComparator + )} ${params.threshold}`; const checksum = sha256.create().update(JSON.stringify(params)); - const link = `${publicBaseUrl}/app/discover#/viewAlert/${alertId}?from=${from}&to=${to}&checksum=${checksum}`; const baseContext: ActionContext = { title: name, - message: `${nrOfDocs} documents found between ${from} and ${to}`, + message: `${matchedDocsNumber} documents found between ${from} and ${to}`, date: timestamp, - value: Number(nrOfDocs), + value: Number(matchedDocsNumber), conditions, - link, + link: `${publicBaseUrl}/app/discover#/viewAlert/${alertId}?from=${from}&to=${to}&checksum=${checksum}`, hits: searchResult.hits.hits, }; const alertInstance = options.services.alertFactory.create(ConditionMetAlertInstanceId); alertInstance.scheduleActions(ActionGroupId, baseContext); } - // this is the state that we can access in the next execution - return { - latestTimestamp: timestamp, - }; + return { latestTimestamp: timestamp }; } From 34cdcfcec41e342dd97c034c1f915b6c3c68debd Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Thu, 24 Feb 2022 17:00:54 +0300 Subject: [PATCH 12/67] Update src/plugins/discover/public/application/view_alert/view_alert_route.tsx Co-authored-by: Matthias Wilhelm --- .../discover/public/application/view_alert/view_alert_route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 856e11cda66b6..fa494a5fdd46d 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -11,7 +11,7 @@ import { sha256 } from 'js-sha256'; import { i18n } from '@kbn/i18n'; import { ToastsStart } from 'kibana/public'; import type { Alert } from '../../../../../../x-pack/plugins/alerting/common'; -import { AlertTypeParams } from '../../../../../../x-pack/plugins/alerting/common'; +import type { AlertTypeParams } from '../../../../../../x-pack/plugins/alerting/common'; import { getTime, SerializedSearchSourceFields } from '../../../../data/common'; import type { Filter, TimeRange } from '../../../../data/public'; import { MarkdownSimple, toMountPoint } from '../../../../kibana_react/public'; From 1332d26e4854d4a203bdec18ed40fa30bb87358c Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 24 Feb 2022 17:10:55 +0300 Subject: [PATCH 13/67] [Discover] add tool tip for data view without time field --- .../data/public/ui/search_bar/index.tsx | 4 +- .../components/top_nav/get_top_nav_links.tsx | 4 +- .../top_nav/open_alerts_popover.tsx | 39 +++++++++++++------ .../view_alert/view_alert_route.tsx | 6 +-- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index 304855610f040..b31b53fd399f2 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -7,6 +7,8 @@ */ import React from 'react'; +import { injectI18n } from '@kbn/i18n-react'; +import { withKibana } from '../../../../kibana_react/public'; import type { SearchBarProps } from './search_bar'; const Fallback = () =>
; @@ -18,6 +20,6 @@ const WrappedSearchBar = (props: SearchBarProps) => ( ); -export const SearchBar = WrappedSearchBar; +export const SearchBar = injectI18n(withKibana(WrappedSearchBar)); export type { StatefulSearchBarProps } from './create_search_bar'; export type { SearchBarProps, SearchBarOwnProps } from './search_bar'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx index 3e1c20b336771..3279cf71a504b 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx @@ -181,9 +181,7 @@ export const getTopNavLinks = ({ ...(services.capabilities.advancedSettings.save ? [options] : []), newSearch, openSearch, - ...(services.triggersActionsUi && savedSearch.searchSource.getField('index')?.timeFieldName - ? [alerts] - : []), + ...(services.triggersActionsUi ? [alerts] : []), shareSearch, inspectSearch, ...(services.capabilities.discover.save ? [saveSearch] : []), diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 3729be612b555..39cced0f93130 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useState, useMemo } from 'react'; import ReactDOM from 'react-dom'; import { I18nStart } from 'kibana/public'; -import { EuiWrappingPopover, EuiLink, EuiContextMenu } from '@elastic/eui'; +import { EuiWrappingPopover, EuiLink, EuiContextMenu, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { ISearchSource } from '../../../../../../data/common'; import { KibanaContextProvider } from '../../../../../../kibana_react/public'; @@ -72,6 +72,30 @@ export function AlertsPopover(props: AlertsPopoverProps) { }); }, [getParams, onCloseAlertFlyout, triggersActionsUi, alertFlyoutVisible]); + const hasTimeFieldName = dataView.timeFieldName; + let createSearchThresholdRuleLink = ( + setAlertFlyoutVisibility(true)} disabled={!hasTimeFieldName}> + + + ); + + if (!hasTimeFieldName) { + const toolTipContent = ( + + ); + createSearchThresholdRuleLink = ( + + {createSearchThresholdRuleLink} + + ); + } + const panels = [ { id: 'mainPanel', @@ -81,20 +105,11 @@ export function AlertsPopover(props: AlertsPopoverProps) { name: ( <> {SearchThresholdAlertFlyout} - { - setAlertFlyoutVisibility(true); - }} - > - - + {createSearchThresholdRuleLink} ), icon: 'bell', - disabled: !dataView.timeFieldName, + disabled: !hasTimeFieldName, }, { name: ( diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index fa494a5fdd46d..8577f65dc7344 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -58,9 +58,7 @@ const displayRuleChangedWarn = (toastNotifications: ToastsStart) => { const getCurrentChecksum = (params: SearchThresholdAlertParams) => sha256.create().update(JSON.stringify(params)).hex(); -const isConcreteAlert = ( - queryParams: QueryParams -): queryParams is NonNullableEntry => { +const isActualAlert = (queryParams: QueryParams): queryParams is NonNullableEntry => { return Boolean(queryParams.from && queryParams.to && queryParams.checksum); }; @@ -81,7 +79,7 @@ export function ViewAlertRoute() { [query] ); - const openConcreteAlert = isConcreteAlert(queryParams); + const openConcreteAlert = isActualAlert(queryParams); useEffect(() => { const fetchAlert = async () => { From 988d4bfc20daecd684d461bfa13f08eb701ddd01 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 24 Feb 2022 17:42:54 +0300 Subject: [PATCH 14/67] [Discover] add info alert about possible document difference that triggered alert and displayed documents --- .../view_alert/view_alert_route.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 8577f65dc7344..f09f738de1ed1 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -55,6 +55,24 @@ const displayRuleChangedWarn = (toastNotifications: ToastsStart) => { }); }; +const displayPossibleDocsDiffInfoAlert = (toastNotifications: ToastsStart) => { + const infoTitle = i18n.translate('discover.viewAlert.possibleDocsDifferenceInfoTitle', { + defaultMessage: 'Possibly different documents', + }); + const infoDescription = i18n.translate( + 'discover.viewAlert.possibleDocsDifferenceInfoDescription', + { + defaultMessage: `Displayed documents might not match the documents triggered + notification, since documents might have been deleted or added.`, + } + ); + + toastNotifications.addInfo({ + title: infoTitle, + text: toMountPoint({infoDescription}), + }); +}; + const getCurrentChecksum = (params: SearchThresholdAlertParams) => sha256.create().update(JSON.stringify(params)).hex(); @@ -124,8 +142,11 @@ export function ViewAlertRoute() { return; } - if (openConcreteAlert && getCurrentChecksum(fetchedAlert.params) !== queryParams.checksum) { + const calculatedChecksum = getCurrentChecksum(fetchedAlert.params); + if (openConcreteAlert && calculatedChecksum !== queryParams.checksum) { displayRuleChangedWarn(toastNotifications); + } else if (openConcreteAlert && calculatedChecksum === queryParams.checksum) { + displayPossibleDocsDiffInfoAlert(toastNotifications); } const fetchedSearchSource = await fetchSearchSource(fetchedAlert); From fba6d5e7062e2a8d208d25994ba781a0e1f868f6 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 24 Feb 2022 17:46:38 +0300 Subject: [PATCH 15/67] [Discover] update unit test --- x-pack/plugins/alerting/server/plugin.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 3716ecfcc1260..4173451268f74 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -19,6 +19,7 @@ import { AlertsConfig } from './config'; import { RuleType } from './types'; import { eventLogMock } from '../../event_log/server/mocks'; import { actionsMock } from '../../actions/server/mocks'; +import { dataPluginMock } from '../../../../src/plugins/data/server/mocks'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -221,6 +222,7 @@ describe('Alerting Plugin', () => { licensing: licensingMock.createStart(), eventLog: eventLogMock.createStart(), taskManager: taskManagerMock.createStart(), + data: dataPluginMock.createStartContract(), }); expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false); @@ -266,6 +268,7 @@ describe('Alerting Plugin', () => { licensing: licensingMock.createStart(), eventLog: eventLogMock.createStart(), taskManager: taskManagerMock.createStart(), + data: dataPluginMock.createStartContract(), }); const fakeRequest = { @@ -322,6 +325,7 @@ describe('Alerting Plugin', () => { licensing: licensingMock.createStart(), eventLog: eventLogMock.createStart(), taskManager: taskManagerMock.createStart(), + data: dataPluginMock.createStartContract(), }); const fakeRequest = { From 6e7a0a3677207905e2ea5e21ab65cce3ee75aab1 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 25 Feb 2022 11:13:23 +0300 Subject: [PATCH 16/67] [Discover] fix unit tests --- .../server/utils/create_lifecycle_rule_type.test.ts | 2 ++ .../rule_registry/server/utils/rule_executor_test_utils.ts | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index baa60664dea57..a889747760bc1 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -18,6 +18,7 @@ import { castArray, omit } from 'lodash'; import { RuleDataClient } from '../rule_data_client'; import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_factory'; +import { ISearchStartSearchSource } from '../../../../../src/plugins/data/common'; type RuleTestHelpers = ReturnType; @@ -115,6 +116,7 @@ function createRule(shouldWriteAlerts: boolean = true) { shouldWriteAlerts: () => shouldWriteAlerts, shouldStopExecution: () => false, search: {} as any, + searchSourceClient: Promise.resolve({} as ISearchStartSearchSource), }, spaceId: 'spaceId', state, diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts index 3d880988182b1..1b749877cc95f 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -7,6 +7,7 @@ import { elasticsearchServiceMock, savedObjectsClientMock, + httpServerMock, } from '../../../../../src/core/server/mocks'; import { AlertExecutorOptions, @@ -16,6 +17,7 @@ import { AlertTypeState, } from '../../../alerting/server'; import { alertsMock } from '../../../alerting/server/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; export const createDefaultAlertExecutorOptions = < Params extends AlertTypeParams = never, @@ -73,6 +75,11 @@ export const createDefaultAlertExecutorOptions = < shouldWriteAlerts: () => shouldWriteAlerts, shouldStopExecution: () => false, search: alertsMock.createAlertServices().search, + searchSourceClient: Promise.resolve( + dataPluginMock + .createStartContract() + .search.searchSource.asScoped(httpServerMock.createKibanaRequest()) + ), }, state, updatedBy: null, From 2528f2f536199d3289341875b3e8794d80d950c1 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Fri, 25 Feb 2022 12:19:19 +0300 Subject: [PATCH 17/67] Update x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../es_query/expression/search_source_expression.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index af4c72cc0c705..1fea86d5bb2d3 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -150,7 +150,7 @@ export const SearchSourceExpression = ({ From f8667d551471e9a714c6d2a288e77836f8989ff2 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Fri, 25 Feb 2022 21:45:32 +0300 Subject: [PATCH 18/67] Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../server/alert_types/es_query/alert_type/alert_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index acc522998051b..a6bf66e44c36c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -120,7 +120,7 @@ export function getAlertType( const actionVariableSearchTypeLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextSearchTypeLabel', { - defaultMessage: 'The type of search is used.', + defaultMessage: 'The type of search.', } ); From 5af2b95008cee76396c8d9c6f9d9c682495a4b04 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Fri, 25 Feb 2022 21:49:53 +0300 Subject: [PATCH 19/67] Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../server/alert_types/es_query/alert_type/alert_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index a6bf66e44c36c..2f7ab69d34a11 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -135,7 +135,7 @@ export function getAlertType( const actionVariableContextLinkLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', { - defaultMessage: `A link to see records that triggered this alert if it created from discover. + defaultMessage: `A link to the records that triggered this alert, if it was created from Discover. In case of Elastic query alert, this link will navigate to stack management.`, } ); From 8d442822360946d266bcf479411ee6331db734de Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Fri, 25 Feb 2022 21:54:53 +0300 Subject: [PATCH 20/67] Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../server/alert_types/es_query/alert_type/alert_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index 2f7ab69d34a11..7167676dddbee 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -136,7 +136,7 @@ export function getAlertType( 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', { defaultMessage: `A link to the records that triggered this alert, if it was created from Discover. - In case of Elastic query alert, this link will navigate to stack management.`, + For Elastic query alerts, this link navigates to Stack Management.`, } ); From 377f035f7e9785624d3df939fa72100d85461407 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Fri, 25 Feb 2022 21:56:29 +0300 Subject: [PATCH 21/67] Update src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../application/main/components/top_nav/open_alerts_popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index cced23b142763..08536a06bf8ab 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -86,7 +86,7 @@ export function AlertsPopover(props: AlertsPopoverProps) { const toolTipContent = ( ); createSearchThresholdRuleLink = ( From 02b6e43317f5f8fe6e736b5ca7af1945ad458ef5 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Sun, 27 Feb 2022 17:30:03 +0300 Subject: [PATCH 22/67] Update x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../es_query/expression/search_source_expression.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 1fea86d5bb2d3..69fab169a49a6 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -183,7 +183,7 @@ export const SearchSourceExpression = ({
From 1cd93a08f8e4eaa3873a86f303b04a8b967af8f4 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 28 Feb 2022 11:27:31 +0300 Subject: [PATCH 23/67] [Discover] fix unit tests --- .../plugins/alerting/server/task_runner/task_runner.test.ts | 3 +++ .../alerting/server/task_runner/task_runner_cancel.test.ts | 3 +++ .../alerting/server/task_runner/task_runner_factory.test.ts | 3 +++ .../alert_types/es_query/alert_type/alert_type.test.ts | 6 +++--- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 99feefb472df1..063ab545914bb 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -64,6 +64,7 @@ import { DATE_1970_5_MIN, } from './fixtures'; import { EVENT_LOG_ACTIONS } from '../plugin'; +import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -95,6 +96,7 @@ describe('Task Runner', () => { const ruleTypeRegistry = ruleTypeRegistryMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); const elasticsearchService = elasticsearchServiceMock.createInternalStart(); + const dataPlugin = dataPluginMock.createStartContract(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -105,6 +107,7 @@ describe('Task Runner', () => { type EnqueueFunction = (options: ExecuteOptions) => Promise; const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { + data: dataPlugin, savedObjects: savedObjectsService, elasticsearch: elasticsearchService, actionsPlugin: actionsMock.createStart(), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index d70b36ff48a8f..d81b25a65fa84 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -34,6 +34,7 @@ import { IEventLogger } from '../../../event_log/server'; import { Alert, RecoveredActionGroup } from '../../common'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; +import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; jest.mock('uuid', () => ({ v4: () => '5f6aa57d-3e22-484e-bae8-cbed868f4d28', @@ -94,6 +95,7 @@ describe('Task Runner Cancel', () => { const ruleTypeRegistry = ruleTypeRegistryMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); const elasticsearchService = elasticsearchServiceMock.createInternalStart(); + const dataPlugin = dataPluginMock.createStartContract(); type TaskRunnerFactoryInitializerParamsType = jest.Mocked & { actionsPlugin: jest.Mocked; @@ -102,6 +104,7 @@ describe('Task Runner Cancel', () => { }; const taskRunnerFactoryInitializerParams: TaskRunnerFactoryInitializerParamsType = { + data: dataPlugin, savedObjects: savedObjectsService, elasticsearch: elasticsearchService, actionsPlugin: actionsMock.createStart(), diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 6dea8df475503..c0305106647fe 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -23,12 +23,14 @@ import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { executionContextServiceMock } from '../../../../../src/core/server/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; const executionContext = executionContextServiceMock.createSetupContract(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); const elasticsearchService = elasticsearchServiceMock.createInternalStart(); +const dataPlugin = dataPluginMock.createStartContract(); const ruleType: UntypedNormalizedRuleType = { id: 'test', name: 'My test alert', @@ -76,6 +78,7 @@ describe('Task Runner Factory', () => { const rulesClient = rulesClientMock.create(); const taskRunnerFactoryInitializerParams: jest.Mocked = { + data: dataPlugin, savedObjects: savedObjectsService, elasticsearch: elasticsearchService, getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts index d119be34bdcd5..e5c058418476d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts @@ -66,8 +66,8 @@ describe('alertType', () => { "name": "conditions", }, Object { - "description": "A link to see records that triggered this alert if it created from discover. - In case of Elastic query alert, this link will navigate to stack management.", + "description": "A link to the records that triggered this alert, if it was created from Discover. + For Elastic query alerts, this link navigates to Stack Management.", "name": "link", }, ], @@ -85,7 +85,7 @@ describe('alertType', () => { "name": "thresholdComparator", }, Object { - "description": "The type of search is used.", + "description": "The type of search.", "name": "searchType", }, Object { From a41aeb47bf08967b03b8ec01f31ef0a66593aec5 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 28 Feb 2022 12:53:50 +0300 Subject: [PATCH 24/67] [Discover] fix security solution alerts --- .../detection_engine/routes/rules/preview_rules_route.ts | 8 +++++++- x-pack/plugins/security_solution/server/routes/index.ts | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 11396864d802d..4cb6ac83ca617 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -7,7 +7,9 @@ import moment from 'moment'; import uuid from 'uuid'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { StartServicesAccessor } from 'kibana/server'; import { IRuleDataClient } from '../../../../../../rule_registry/server'; +import type { StartPlugins } from '../../../../plugin'; import { buildSiemResponse } from '../utils'; import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters'; import { RuleParams } from '../../schemas/rule_schemas'; @@ -55,7 +57,8 @@ export const previewRulesRoute = async ( security: SetupPlugins['security'], ruleOptions: CreateRuleOptions, securityRuleTypeOptions: CreateSecurityRuleTypeWrapperProps, - previewRuleDataClient: IRuleDataClient + previewRuleDataClient: IRuleDataClient, + getStartServices: StartServicesAccessor ) => { router.post( { @@ -74,6 +77,8 @@ export const previewRulesRoute = async ( return siemResponse.error({ statusCode: 400, body: validationErrors }); } try { + const [, { data }] = await getStartServices(); + const searchSourceClient = data.search.searchSource.asScoped(request); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.securitySolution.getAppClient(); @@ -195,6 +200,7 @@ export const previewRulesRoute = async ( }), savedObjectsClient: context.core.savedObjects.client, scopedClusterClient: context.core.elasticsearch.client, + searchSourceClient, }, spaceId, startedAt: startedAt.toDate(), diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 935948d6d5938..2efb132c96ff6 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -103,7 +103,8 @@ export const initRoutes = ( security, ruleOptions, securityRuleTypeOptions, - previewRuleDataClient + previewRuleDataClient, + getStartServices ); // Once we no longer have the legacy notifications system/"side car actions" this should be removed. From 7889c47c908cd09dad78661f2c448d3f7883b829 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 1 Mar 2022 12:00:58 +0300 Subject: [PATCH 25/67] [Discover] fix eslint errors --- .../es_query/expression/search_source_expression.tsx | 6 +++--- .../server/alert_types/es_query/action_context.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 69fab169a49a6..ff72ef1971a28 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -158,14 +158,14 @@ export const SearchSourceExpression = ({
diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index 2957f21f662f7..2db622ec0d39c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -29,7 +29,7 @@ export interface EsQueryAlertActionContext extends AlertInstanceContext { // threshold conditions conditions: string; // query matches - hits?: estypes.SearchHit[]; + hits: estypes.SearchHit[]; } export function addMessages( From 17b413e0b0e7b18b2e4d8b3e488b4e23c10c6efa Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 1 Mar 2022 13:16:25 +0300 Subject: [PATCH 26/67] [Discover] fix unit tests --- .../es_query/expression/search_source_expression.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx index 4c2ef2f96540b..4b18a5a5f96a2 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx @@ -96,7 +96,7 @@ describe('SearchSourceAlertTypeExpression', () => { ); const wrapper = await setup(defaultSearchSourceExpressionParams); - rerender(wrapper); + await rerender(wrapper); expect(wrapper.find(EuiCallOut).exists()).toBeTruthy(); }); @@ -107,7 +107,7 @@ describe('SearchSourceAlertTypeExpression', () => { ); const wrapper = await setup(defaultSearchSourceExpressionParams); - rerender(wrapper); + await rerender(wrapper); expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); From 347252c85d04b0af542d7f8a2939a6d50daaf799 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 1 Mar 2022 13:50:18 +0300 Subject: [PATCH 27/67] Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../server/alert_types/es_query/alert_type/alert_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index 7167676dddbee..621b3ac619ad8 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -99,7 +99,7 @@ export function getAlertType( 'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel', { defaultMessage: - "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", + "An array of values to use as the threshold. 'between' and 'notBetween' require two values.", } ); From bc381b65dd609bf4ff22608ba56c5bbe4a137a94 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 1 Mar 2022 13:53:06 +0300 Subject: [PATCH 28/67] Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../server/alert_types/es_query/alert_type/alert_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index 621b3ac619ad8..4c58f130b0085 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -106,7 +106,7 @@ export function getAlertType( const actionVariableContextThresholdComparatorLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextThresholdComparatorLabel', { - defaultMessage: 'A function to determine if the threshold has been met.', + defaultMessage: 'A function to determine if the threshold was met.', } ); From 1819a3546e5e41ecedc0ac93cbf1009729d4c190 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 1 Mar 2022 13:55:12 +0300 Subject: [PATCH 29/67] [Discover] apply suggestions --- .../application/view_alert/view_alert_route.tsx | 15 ++++++--------- .../expression/search_source_expression.tsx | 8 +------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index f09f738de1ed1..2f3e6f5b5fcff 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -56,16 +56,13 @@ const displayRuleChangedWarn = (toastNotifications: ToastsStart) => { }; const displayPossibleDocsDiffInfoAlert = (toastNotifications: ToastsStart) => { - const infoTitle = i18n.translate('discover.viewAlert.possibleDocsDifferenceInfoTitle', { - defaultMessage: 'Possibly different documents', + const infoTitle = i18n.translate('discover.viewAlert.documentsMayVaryInfoTitle', { + defaultMessage: 'Displayed documents may vary', + }); + const infoDescription = i18n.translate('discover.viewAlert.documentsMayVaryInfoDescription', { + defaultMessage: `The displayed documents might differ from the documents that triggered the alert. + Some documents might have been added or deleted.`, }); - const infoDescription = i18n.translate( - 'discover.viewAlert.possibleDocsDifferenceInfoDescription', - { - defaultMessage: `Displayed documents might not match the documents triggered - notification, since documents might have been deleted or added.`, - } - ); toastNotifications.addInfo({ title: infoTitle, diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index ff72ef1971a28..5938ff947c328 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -90,13 +90,7 @@ export const SearchSourceExpression = ({ if (paramsError) { return ( <> - +

{paramsError.message}

From b020ba1968ca081c967302398170c0f15f37315e Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 1 Mar 2022 15:06:14 +0300 Subject: [PATCH 30/67] [Discover] fix tests --- .../server/alert_types/es_query/alert_type/alert_type.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts index e5c058418476d..e3c07d002df11 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts @@ -77,11 +77,11 @@ describe('alertType', () => { "name": "size", }, Object { - "description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", + "description": "An array of values to use as the threshold. 'between' and 'notBetween' require two values.", "name": "threshold", }, Object { - "description": "A function to determine if the threshold has been met.", + "description": "A function to determine if the threshold was met.", "name": "thresholdComparator", }, Object { From b6711557b697d63aad7cce1a682c49b012dbdcaf Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 2 Mar 2022 19:48:53 +0100 Subject: [PATCH 31/67] Update x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts --- .../server/alert_types/es_query/alert_type/alert_type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index 4c58f130b0085..162d6f2b88f0b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -192,5 +192,5 @@ export function getAlertType( } function isEsQueryAlert(options: ExecutorOptions) { - return options.params.searchType === 'esQuery'; + return options.params.searchType !== 'searchSource'; } From 111ae940b548801ee7f69548692733d8f54b935d Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 3 Mar 2022 12:37:53 +0300 Subject: [PATCH 32/67] [Discover] remove close button in filters --- src/plugins/data/public/ui/filter_bar/filter_item.tsx | 3 ++- src/plugins/data/public/ui/filter_bar/filter_view/index.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 06ced8d59df90..7f203965a9a16 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -359,6 +359,7 @@ export function FilterItem(props: FilterItemProps) { iconOnClick: () => props.onRemove(), onClick: handleBadgeClick, ['data-test-subj']: getDataTestSubj(valueLabelConfig), + readonly: props.readonly, }; const popoverProps: CommonProps & HTMLAttributes & EuiPopoverProps = { @@ -380,7 +381,7 @@ export function FilterItem(props: FilterItemProps) { anchorPosition="upCenter" {...popoverProps} > - + ); } diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 50e8e54975730..e5345462b7df2 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -19,6 +19,7 @@ interface Props { filterLabelStatus: FilterLabelStatus; errorMessage?: string; readonly?: boolean; + hideAlias?: boolean; [propName: string]: any; } @@ -30,6 +31,7 @@ export const FilterView: FC = ({ errorMessage, filterLabelStatus, readonly, + hideAlias, ...rest }: Props) => { const [ref, innerText] = useInnerText(); @@ -90,7 +92,7 @@ export const FilterView: FC = ({ filter={filter} valueLabel={valueLabel} filterLabelStatus={filterLabelStatus} - hideAlias={readonly} + hideAlias={hideAlias} /> From ad2b1b3544ec4679e0893c67515497e77a7897b9 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 3 Mar 2022 18:17:58 +0100 Subject: [PATCH 33/67] Improve code structure --- .../alert_types/es_query/action_context.ts | 4 +- .../es_query/alert_type/alert_type.test.ts | 11 +- .../es_query/alert_type/alert_type.ts | 31 +-- .../es_query/alert_type/es_query_executor.ts | 211 ------------------ .../es_query/alert_type/executor.ts | 159 +++++++++++++ .../es_query/alert_type/lib/fetch_es_query.ts | 93 ++++++++ .../lib/fetch_search_source_query.ts | 63 ++++++ .../alert_type/lib/get_search_params.ts | 56 +++++ .../es_query/alert_type/messages.ts | 27 +++ .../alert_type/search_source_executor.ts | 91 -------- 10 files changed, 408 insertions(+), 338 deletions(-) delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/es_query_executor.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_search_source_query.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/get_search_params.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/search_source_executor.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts index 2db622ec0d39c..040d4f78fce21 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { AlertExecutorOptions, AlertInstanceContext } from '../../../../alerting/server'; -import { OnlyEsQueryAlertParams } from './types'; +import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; // alert type context provided to actions @@ -35,7 +35,7 @@ export interface EsQueryAlertActionContext extends AlertInstanceContext { export function addMessages( alertInfo: AlertInfo, baseContext: EsQueryAlertActionContext, - params: OnlyEsQueryAlertParams + params: OnlyEsQueryAlertParams | OnlySearchSourceAlertParams ): ActionContext { const title = i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle', { defaultMessage: `alert '{name}' matched query`, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts index e3c07d002df11..bdc09786ae232 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts @@ -66,7 +66,7 @@ describe('alertType', () => { "name": "conditions", }, Object { - "description": "A link to the records that triggered this alert, if it was created from Discover. + "description": "A link to the records that triggered this alert, if it was created from Discover. For Elastic query alerts, this link navigates to Stack Management.", "name": "link", }, @@ -516,9 +516,7 @@ describe('alertType', () => { }); it('validator succeeds with valid search source params', async () => { - const params = defaultParams; - - expect(alertType.validate?.params?.validate(params)).toBeTruthy(); + expect(alertType.validate?.params?.validate(defaultParams)).toBeTruthy(); }); it('validator fails with invalid search source params - esQuery provided', async () => { @@ -574,11 +572,6 @@ describe('alertType', () => { const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); - const scheduleActionsMock = jest.fn(); - alertServices.alertFactory.create.mockImplementationOnce(() => ({ - scheduleActions: scheduleActionsMock, - })); - (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { if (name === 'index') { return dataViewMock; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts index 162d6f2b88f0b..854240f3b1de1 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts @@ -15,10 +15,9 @@ import { EsQueryAlertState, } from '../alert_type_params'; import { STACK_ALERTS_FEATURE_ID } from '../../../../common'; -import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from '../types'; +import { ExecutorOptions } from '../types'; import { ActionGroupId, ES_QUERY_ID } from '../constants'; -import { esQueryExecutor } from './es_query_executor'; -import { searchSourceExecutor } from './search_source_executor'; +import { executor } from './executor'; export function getAlertType( logger: Logger, @@ -135,7 +134,7 @@ export function getAlertType( const actionVariableContextLinkLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', { - defaultMessage: `A link to the records that triggered this alert, if it was created from Discover. + defaultMessage: `A link to the records that triggered this alert, if it was created from Discover. For Elastic query alerts, this link navigates to Stack Management.`, } ); @@ -170,27 +169,9 @@ export function getAlertType( }, minimumLicenseRequired: 'basic', isExportable: true, - executor, + executor: async (options: ExecutorOptions) => { + return await executor(logger, core, options); + }, producer: STACK_ALERTS_FEATURE_ID, }; - - async function executor(options: ExecutorOptions) { - if (isEsQueryAlert(options)) { - return await esQueryExecutor( - logger, - core, - options as ExecutorOptions - ); - } else { - return await searchSourceExecutor( - logger, - core, - options as ExecutorOptions - ); - } - } -} - -function isEsQueryAlert(options: ExecutorOptions) { - return options.params.searchType !== 'searchSource'; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/es_query_executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/es_query_executor.ts deleted file mode 100644 index e6cb2dcee8423..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/es_query_executor.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { CoreSetup, Logger } from 'src/core/server'; -import { EsQueryAlertActionContext, addMessages } from '../action_context'; -import { ComparatorFns, getHumanReadableComparator } from '../../lib'; -import { parseDuration } from '../../../../../alerting/server'; -import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query'; -import { ExecutorOptions, OnlyEsQueryAlertParams } from '../types'; -import { ActionGroupId, ConditionMetAlertInstanceId, ES_QUERY_ID } from '../constants'; - -export async function esQueryExecutor( - logger: Logger, - core: CoreSetup, - options: ExecutorOptions -) { - const { alertId, name, services, params, state } = options; - const { alertFactory, search } = services; - const previousTimestamp = state.latestTimestamp; - const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; - - const abortableEsClient = search.asCurrentUser; - const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); - - const compareFn = ComparatorFns.get(params.thresholdComparator); - if (compareFn == null) { - throw new Error(getInvalidComparatorError(params.thresholdComparator)); - } - - // During each alert execution, we run the configured query, get a hit count - // (hits.total) and retrieve up to params.size hits. We - // evaluate the threshold condition using the value of hits.total. If the threshold - // condition is met, the hits are counted toward the query match and we update - // the alert state with the timestamp of the latest hit. In the next execution - // of the alert, the latestTimestamp will be used to gate the query in order to - // avoid counting a document multiple times. - - let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); - const filter = timestamp - ? { - bool: { - filter: [ - parsedQuery.query, - { - bool: { - must_not: [ - { - bool: { - filter: [ - { - range: { - [params.timeField]: { - lte: timestamp, - format: 'strict_date_optional_time', - }, - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - } - : parsedQuery.query; - - const query = buildSortedEventsQuery({ - index: params.index, - from: dateStart, - to: dateEnd, - filter, - size: params.size, - sortOrder: 'desc', - searchAfterSortId: undefined, - timeField: params.timeField, - track_total_hits: true, - }); - - logger.debug( - `es query alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}` - ); - - const { body: searchResult } = await abortableEsClient.search(query); - - logger.debug( - ` es query alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` - ); - - const numMatches = (searchResult.hits.total as estypes.SearchTotalHits).value; - - // apply the alert condition - const conditionMet = compareFn(numMatches, params.threshold); - - if (conditionMet) { - const humanFn = i18n.translate( - 'xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', - { - defaultMessage: `Number of matching documents is {thresholdComparator} {threshold}`, - values: { - thresholdComparator: getHumanReadableComparator(params.thresholdComparator), - threshold: params.threshold.join(' and '), - }, - } - ); - - const baseContext: EsQueryAlertActionContext = { - date: new Date().toISOString(), - value: numMatches, - conditions: humanFn, - hits: searchResult.hits.hits, - link: `${publicBaseUrl}/app/management/insightsAndAlerting/triggersActions/rule/${alertId}`, - }; - - const actionContext = addMessages(options, baseContext, params); - const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); - alertInstance - // store the params we would need to recreate the query that led to this alert instance - .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) - .scheduleActions(ActionGroupId, actionContext); - - // update the timestamp based on the current search results - const firstValidTimefieldSort = getValidTimefieldSort( - searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort - ); - if (firstValidTimefieldSort) { - timestamp = firstValidTimefieldSort; - } - } - - return { latestTimestamp: timestamp }; -} - -function getInvalidComparatorError(comparator: string) { - return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator, - }, - }); -} - -function getInvalidWindowSizeError(windowValue: string) { - return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', { - defaultMessage: 'invalid format for windowSize: "{windowValue}"', - values: { - windowValue, - }, - }); -} - -function getInvalidQueryError(query: string) { - return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', { - defaultMessage: 'invalid query specified: "{query}" - query must be JSON', - values: { - query, - }, - }); -} - -function getSearchParams(queryParams: OnlyEsQueryAlertParams) { - const date = Date.now(); - const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; - - let parsedQuery; - try { - parsedQuery = JSON.parse(esQuery); - } catch (err) { - throw new Error(getInvalidQueryError(esQuery)); - } - - if (parsedQuery && !parsedQuery.query) { - throw new Error(getInvalidQueryError(esQuery)); - } - - const window = `${timeWindowSize}${timeWindowUnit}`; - let timeWindow: number; - try { - timeWindow = parseDuration(window); - } catch (err) { - throw new Error(getInvalidWindowSizeError(window)); - } - - const dateStart = new Date(date - timeWindow).toISOString(); - const dateEnd = new Date(date).toISOString(); - - return { parsedQuery, dateStart, dateEnd }; -} - -function getValidTimefieldSort(sortValues: Array = []): undefined | string { - for (const sortValue of sortValues) { - const sortDate = tryToParseAsDate(sortValue); - if (sortDate) { - return sortDate; - } - } -} - -function tryToParseAsDate(sortValue?: string | number | null): undefined | string { - const sortDate = typeof sortValue === 'string' ? Date.parse(sortValue) : sortValue; - if (sortDate && !isNaN(sortDate)) { - return new Date(sortDate).toISOString(); - } -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts new file mode 100644 index 0000000000000..3c325461d28cb --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts @@ -0,0 +1,159 @@ +/* + * 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 { sha256 } from 'js-sha256'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, Logger } from 'src/core/server'; +import { addMessages, EsQueryAlertActionContext } from '../action_context'; +import { ComparatorFns } from '../../lib'; +import { parseDuration } from '../../../../../alerting/server'; +import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from '../types'; +import { ActionGroupId, ConditionMetAlertInstanceId } from '../constants'; +import { getContextConditionsDescription, getInvalidComparatorError } from './messages'; +import { fetchEsQuery } from './lib/fetch_es_query'; +import { EsQueryAlertParams } from '../alert_type_params'; +import { fetchSearchSourceQuery } from './lib/fetch_search_source_query'; + +export async function executor( + logger: Logger, + core: CoreSetup, + options: ExecutorOptions +) { + const esQueryAlert = isEsQueryAlert(options); + const { alertId, name, services, params, state } = options; + const { alertFactory, search, searchSourceClient } = services; + const currentTimestamp = new Date().toISOString(); + const previousTimestamp = state.latestTimestamp; + const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; + + const compareFn = ComparatorFns.get(params.thresholdComparator); + if (compareFn == null) { + throw new Error(getInvalidComparatorError(params.thresholdComparator)); + } + let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); + + const { numMatches, searchResult, dateStart, dateEnd } = esQueryAlert + ? await fetchEsQuery(alertId, params as OnlyEsQueryAlertParams, timestamp, { search, logger }) + : await fetchSearchSourceQuery(alertId, params as OnlySearchSourceAlertParams, timestamp, { + searchSourceClient, + logger, + }); + + // apply the alert condition + const conditionMet = compareFn(numMatches, params.threshold); + + if (conditionMet) { + const base = publicBaseUrl; + const link = esQueryAlert + ? `${base}/app/management/insightsAndAlerting/triggersActions/rule/${alertId}` + : `${base}/app/discover#/viewAlert/${alertId}?from=${dateStart}&to=${dateEnd}&checksum=${getChecksum( + params + )}`; + + const conditions = getContextConditionsDescription( + params.thresholdComparator, + params.threshold + ); + const baseContext: EsQueryAlertActionContext = { + title: name, + date: currentTimestamp, + value: numMatches, + conditions, + hits: searchResult.hits.hits, + link, + }; + + const actionContext = addMessages(options, baseContext, params); + const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); + alertInstance + // store the params we would need to recreate the query that led to this alert instance + .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) + .scheduleActions(ActionGroupId, actionContext); + + // update the timestamp based on the current search results + const firstValidTimefieldSort = getValidTimefieldSort( + searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort + ); + if (firstValidTimefieldSort) { + timestamp = firstValidTimefieldSort; + } + } + + return { latestTimestamp: timestamp }; +} + +function getInvalidWindowSizeError(windowValue: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', { + defaultMessage: 'invalid format for windowSize: "{windowValue}"', + values: { + windowValue, + }, + }); +} + +function getInvalidQueryError(query: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', { + defaultMessage: 'invalid query specified: "{query}" - query must be JSON', + values: { + query, + }, + }); +} + +export function getSearchParams(queryParams: OnlyEsQueryAlertParams) { + const date = Date.now(); + const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; + + let parsedQuery; + try { + parsedQuery = JSON.parse(esQuery); + } catch (err) { + throw new Error(getInvalidQueryError(esQuery)); + } + + if (parsedQuery && !parsedQuery.query) { + throw new Error(getInvalidQueryError(esQuery)); + } + + const window = `${timeWindowSize}${timeWindowUnit}`; + let timeWindow: number; + try { + timeWindow = parseDuration(window); + } catch (err) { + throw new Error(getInvalidWindowSizeError(window)); + } + + const dateStart = new Date(date - timeWindow).toISOString(); + const dateEnd = new Date(date).toISOString(); + + return { parsedQuery, dateStart, dateEnd }; +} + +export function getValidTimefieldSort( + sortValues: Array = [] +): undefined | string { + for (const sortValue of sortValues) { + const sortDate = tryToParseAsDate(sortValue); + if (sortDate) { + return sortDate; + } + } +} + +export function tryToParseAsDate(sortValue?: string | number | null): undefined | string { + const sortDate = typeof sortValue === 'string' ? Date.parse(sortValue) : sortValue; + if (sortDate && !isNaN(sortDate)) { + return new Date(sortDate).toISOString(); + } +} + +export function isEsQueryAlert(options: ExecutorOptions) { + return options.params.searchType !== 'searchSource'; +} + +export function getChecksum(params: EsQueryAlertParams) { + return sha256.create().update(JSON.stringify(params)); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts new file mode 100644 index 0000000000000..f5209c59b8120 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts @@ -0,0 +1,93 @@ +/* + * 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 'kibana/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { OnlyEsQueryAlertParams } from '../../types'; +import { IAbortableClusterClient } from '../../../../../../alerting/server'; +import { buildSortedEventsQuery } from '../../../../../common/build_sorted_events_query'; +import { ES_QUERY_ID } from '../../constants'; +import { getSearchParams } from './get_search_params'; + +export async function fetchEsQuery( + alertId: string, + params: OnlyEsQueryAlertParams, + timestamp: string | undefined, + services: { + search: IAbortableClusterClient; + logger: Logger; + } +) { + const { search, logger } = services; + const abortableEsClient = search.asCurrentUser; + const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); + + // During each alert execution, we run the configured query, get a hit count + // (hits.total) and retrieve up to params.size hits. We + // evaluate the threshold condition using the value of hits.total. If the threshold + // condition is met, the hits are counted toward the query match and we update + // the alert state with the timestamp of the latest hit. In the next execution + // of the alert, the latestTimestamp will be used to gate the query in order to + // avoid counting a document multiple times. + + const filter = timestamp + ? { + bool: { + filter: [ + parsedQuery.query, + { + bool: { + must_not: [ + { + bool: { + filter: [ + { + range: { + [params.timeField]: { + lte: timestamp, + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + } + : parsedQuery.query; + + const query = buildSortedEventsQuery({ + index: params.index, + from: dateStart, + to: dateEnd, + filter, + size: params.size, + sortOrder: 'desc', + searchAfterSortId: undefined, + timeField: params.timeField, + track_total_hits: true, + }); + + logger.debug( + `es query alert ${ES_QUERY_ID}:${alertId} "${name}" query - ${JSON.stringify(query)}` + ); + + const { body: searchResult } = await abortableEsClient.search(query); + + logger.debug( + ` es query alert ${ES_QUERY_ID}:${alertId} "${name}" result - ${JSON.stringify(searchResult)}` + ); + return { + numMatches: (searchResult.hits.total as estypes.SearchTotalHits).value, + searchResult, + dateStart, + dateEnd, + }; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_search_source_query.ts new file mode 100644 index 0000000000000..7ba279f36f553 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_search_source_query.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 { buildRangeFilter, Filter } from '@kbn/es-query'; +import { Logger } from 'kibana/server'; +import { OnlySearchSourceAlertParams } from '../../types'; +import { getTime, ISearchStartSearchSource } from '../../../../../../../../src/plugins/data/common'; + +export async function fetchSearchSourceQuery( + alertId: string, + params: OnlySearchSourceAlertParams, + timestamp: string | undefined, + services: { + logger: Logger; + searchSourceClient: Promise; + } +) { + const { logger, searchSourceClient } = services; + const client = await searchSourceClient; + const loadedSearchSource = await client.create(params.searchConfiguration); + const index = loadedSearchSource.getField('index'); + + const timeFieldName = index?.timeFieldName; + if (!timeFieldName) { + throw new Error('Invalid data view without timeFieldName.'); + } + + loadedSearchSource.setField('size', params.size); + + const timerangeFilter = getTime(index, { + from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, + to: 'now', + }); + const dateStart = timerangeFilter?.query.range[timeFieldName].gte; + const dateEnd = timerangeFilter?.query.range[timeFieldName].lte; + const filters = [timerangeFilter]; + + if (timestamp) { + const field = index.fields.find((f) => f.name === timeFieldName); + const addTimeRangeField = buildRangeFilter(field!, { gt: timestamp }, index); + filters.push(addTimeRangeField); + } + const searchSourceChild = loadedSearchSource.createChild(); + searchSourceChild.setField('filter', filters as Filter[]); + + logger.debug( + `search source query alert (${alertId}) query: ${JSON.stringify( + searchSourceChild.getSearchRequestBody() + )}` + ); + + const searchResult = await searchSourceChild.fetch(); + + return { + numMatches: Number(searchResult.hits.total), + searchResult, + dateStart, + dateEnd, + }; +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/get_search_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/get_search_params.ts new file mode 100644 index 0000000000000..8fa165523cba7 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/get_search_params.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { OnlyEsQueryAlertParams } from '../../types'; +import { parseDuration } from '../../../../../../alerting/common'; + +export function getSearchParams(queryParams: OnlyEsQueryAlertParams) { + const date = Date.now(); + const { esQuery, timeWindowSize, timeWindowUnit } = queryParams; + + let parsedQuery; + try { + parsedQuery = JSON.parse(esQuery); + } catch (err) { + throw new Error(getInvalidQueryError(esQuery)); + } + + if (parsedQuery && !parsedQuery.query) { + throw new Error(getInvalidQueryError(esQuery)); + } + + const window = `${timeWindowSize}${timeWindowUnit}`; + let timeWindow: number; + try { + timeWindow = parseDuration(window); + } catch (err) { + throw new Error(getInvalidWindowSizeError(window)); + } + + const dateStart = new Date(date - timeWindow).toISOString(); + const dateEnd = new Date(date).toISOString(); + + return { parsedQuery, dateStart, dateEnd }; +} + +function getInvalidWindowSizeError(windowValue: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidWindowSizeErrorMessage', { + defaultMessage: 'invalid format for windowSize: "{windowValue}"', + values: { + windowValue, + }, + }); +} + +function getInvalidQueryError(query: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidQueryErrorMessage', { + defaultMessage: 'invalid query specified: "{query}" - query must be JSON', + values: { + query, + }, + }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts new file mode 100644 index 0000000000000..24cf4fe247055 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { getHumanReadableComparator } from '../../lib'; + +export function getInvalidComparatorError(comparator: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); +} + +export function getContextConditionsDescription(comparator: string, threshold: number[]) { + return i18n.translate('Number of matching documents is {comparator} {threshold}', { + defaultMessage: 'Number of matching documents is {comparator} {threshold}', + values: { + comparator: getHumanReadableComparator(comparator), + threshold: threshold.join(' and '), + }, + }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/search_source_executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/search_source_executor.ts deleted file mode 100644 index 201d24e50061f..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/search_source_executor.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { sha256 } from 'js-sha256'; -import { CoreSetup, Logger } from 'src/core/server'; -import { getTime } from '../../../../../../../src/plugins/data/common'; -import { ActionContext } from '../action_context'; -import { ComparatorFns, getHumanReadableComparator } from '../../lib'; -import { ExecutorOptions, OnlySearchSourceAlertParams } from '../types'; -import { ActionGroupId, ConditionMetAlertInstanceId } from '../constants'; - -export async function searchSourceExecutor( - logger: Logger, - core: CoreSetup, - options: ExecutorOptions -) { - const { name, params, alertId, services } = options; - const timestamp = new Date().toISOString(); - const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; - - const compareFn = ComparatorFns.get(params.thresholdComparator); - if (compareFn == null) { - throw new Error( - i18n.translate('xpack.stackAlerts.searchThreshold.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator: params.thresholdComparator, - }, - }) - ); - } - - const searchSourceClient = await services.searchSourceClient; - const loadedSearchSource = await searchSourceClient.create(params.searchConfiguration); - const index = loadedSearchSource.getField('index'); - - const timeFieldName = index?.timeFieldName; - if (!timeFieldName) { - throw new Error('Invalid data view without timeFieldName.'); - } - - loadedSearchSource.setField('size', params.size); - - const filter = getTime(index, { - from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, - to: 'now', - }); - const from = filter?.query.range[timeFieldName].gte; - const to = filter?.query.range[timeFieldName].lte; - const searchSourceChild = loadedSearchSource.createChild(); - searchSourceChild.setField('filter', filter); - - logger.debug( - `search source query alert (${alertId}) query: ${JSON.stringify( - searchSourceChild.getSearchRequestBody() - )}` - ); - - const searchResult = await searchSourceChild.fetch(); - const matchedDocsNumber = Number(searchResult.hits.total); - - logger.debug( - `search source query alert (${alertId}) number of matched documents: ${matchedDocsNumber}` - ); - - const met = compareFn(matchedDocsNumber, params.threshold); - if (met) { - const conditions = `${matchedDocsNumber} is ${getHumanReadableComparator( - params.thresholdComparator - )} ${params.threshold}`; - const checksum = sha256.create().update(JSON.stringify(params)); - const baseContext: ActionContext = { - title: name, - message: `${matchedDocsNumber} documents found between ${from} and ${to}`, - date: timestamp, - value: Number(matchedDocsNumber), - conditions, - link: `${publicBaseUrl}/app/discover#/viewAlert/${alertId}?from=${from}&to=${to}&checksum=${checksum}`, - hits: searchResult.hits.hits, - }; - const alertInstance = options.services.alertFactory.create(ConditionMetAlertInstanceId); - alertInstance.scheduleActions(ActionGroupId, baseContext); - } - - return { latestTimestamp: timestamp }; -} From 50f9f470fd8419c83d39f1ee9f16a6235b1a3e57 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 4 Mar 2022 08:07:55 +0100 Subject: [PATCH 34/67] Fix missing name in fetchEsQuery --- .../server/alert_types/es_query/alert_type/executor.ts | 5 ++++- .../alert_types/es_query/alert_type/lib/fetch_es_query.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts index 3c325461d28cb..da2a459c046df 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts @@ -36,7 +36,10 @@ export async function executor( let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); const { numMatches, searchResult, dateStart, dateEnd } = esQueryAlert - ? await fetchEsQuery(alertId, params as OnlyEsQueryAlertParams, timestamp, { search, logger }) + ? await fetchEsQuery(alertId, name, params as OnlyEsQueryAlertParams, timestamp, { + search, + logger, + }) : await fetchSearchSourceQuery(alertId, params as OnlySearchSourceAlertParams, timestamp, { searchSourceClient, logger, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts index f5209c59b8120..917a5a33b1132 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts @@ -14,6 +14,7 @@ import { getSearchParams } from './get_search_params'; export async function fetchEsQuery( alertId: string, + name: string, params: OnlyEsQueryAlertParams, timestamp: string | undefined, services: { From a60d7dce5fd95d0db1a5528c08b1e2f1c29e5bd0 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 4 Mar 2022 11:05:19 +0100 Subject: [PATCH 35/67] Fix messages --- .../server/alert_types/es_query/alert_type/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts index 24cf4fe247055..b5dc9620537a4 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts @@ -17,7 +17,7 @@ export function getInvalidComparatorError(comparator: string) { } export function getContextConditionsDescription(comparator: string, threshold: number[]) { - return i18n.translate('Number of matching documents is {comparator} {threshold}', { + return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { defaultMessage: 'Number of matching documents is {comparator} {threshold}', values: { comparator: getHumanReadableComparator(comparator), From 8a9dcb3b9acd37212aef4f213338fa85e2d29637 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 4 Mar 2022 12:05:35 +0100 Subject: [PATCH 36/67] Fix messages, again --- .../server/alert_types/es_query/alert_type/messages.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts index b5dc9620537a4..60a90e4ac9404 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts @@ -18,9 +18,9 @@ export function getInvalidComparatorError(comparator: string) { export function getContextConditionsDescription(comparator: string, threshold: number[]) { return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { - defaultMessage: 'Number of matching documents is {comparator} {threshold}', + defaultMessage: 'Number of matching documents is {thresholdComparator} {threshold}', values: { - comparator: getHumanReadableComparator(comparator), + thresholdComparator: getHumanReadableComparator(comparator), threshold: threshold.join(' and '), }, }); From 2eb04f38940aa975efbae1684163e76150817df1 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 7 Mar 2022 08:14:35 +0100 Subject: [PATCH 37/67] Refactor --- .../{alert_type => }/alert_type.test.ts | 23 ++++++------- .../es_query/{alert_type => }/alert_type.ts | 14 ++++---- .../alert_types/es_query/alert_type/index.ts | 8 ----- .../es_query/alert_type/messages.ts | 27 --------------- .../es_query/{alert_type => }/executor.ts | 34 ++++++++++++++----- .../{alert_type => }/lib/fetch_es_query.ts | 8 ++--- .../lib/fetch_search_source_query.ts | 4 +-- .../{alert_type => }/lib/get_search_params.ts | 4 +-- 8 files changed, 51 insertions(+), 71 deletions(-) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type => }/alert_type.test.ts (96%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type => }/alert_type.ts (94%) delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/index.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type => }/executor.ts (82%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type => }/lib/fetch_es_query.ts (91%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type => }/lib/fetch_search_source_query.ts (95%) rename x-pack/plugins/stack_alerts/server/alert_types/es_query/{alert_type => }/lib/get_search_params.ts (92%) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts similarity index 96% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index bdc09786ae232..ccc4c244ec2c2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -7,25 +7,22 @@ import uuid from 'uuid'; import type { Writable } from '@kbn/utility-types'; -import { AlertServices } from '../../../../../alerting/server'; +import { AlertServices } from '../../../../alerting/server'; import { AlertServicesMock, alertsMock, AlertInstanceMock, -} from '../../../../../alerting/server/mocks'; -import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; +} from '../../../../alerting/server/mocks'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { getAlertType } from './alert_type'; -import { EsQueryAlertParams, EsQueryAlertState } from '../alert_type_params'; -import { ActionContext } from '../action_context'; -import { - ESSearchResponse, - ESSearchRequest, -} from '../../../../../../../src/core/types/elasticsearch'; +import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; +import { ActionContext } from './action_context'; +import { ESSearchResponse, ESSearchRequest } from '../../../../../../src/core/types/elasticsearch'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { elasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; -import { coreMock } from '../../../../../../../src/core/server/mocks'; -import { ActionGroupId, ConditionMetAlertInstanceId } from '../constants'; -import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from '../types'; +import { elasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; +import { coreMock } from '../../../../../../src/core/server/mocks'; +import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; +import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks'; const logger = loggingSystemMock.create().get(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts similarity index 94% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 854240f3b1de1..3f904b9142555 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -6,17 +6,17 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, Logger } from 'src/core/server'; -import { RuleType } from '../../../types'; -import { ActionContext } from '../action_context'; +import { CoreSetup, Logger } from 'kibana/server'; +import { RuleType } from '../../types'; +import { ActionContext } from './action_context'; import { EsQueryAlertParams, EsQueryAlertParamsSchema, EsQueryAlertState, -} from '../alert_type_params'; -import { STACK_ALERTS_FEATURE_ID } from '../../../../common'; -import { ExecutorOptions } from '../types'; -import { ActionGroupId, ES_QUERY_ID } from '../constants'; +} from './alert_type_params'; +import { STACK_ALERTS_FEATURE_ID } from '../../../common'; +import { ExecutorOptions } from './types'; +import { ActionGroupId, ES_QUERY_ID } from './constants'; import { executor } from './executor'; export function getAlertType( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/index.ts deleted file mode 100644 index d670d9cb7c566..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/index.ts +++ /dev/null @@ -1,8 +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. - */ - -export { getAlertType } from './alert_type'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts deleted file mode 100644 index 60a90e4ac9404..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/messages.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { i18n } from '@kbn/i18n'; -import { getHumanReadableComparator } from '../../lib'; - -export function getInvalidComparatorError(comparator: string) { - return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator, - }, - }); -} - -export function getContextConditionsDescription(comparator: string, threshold: number[]) { - return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { - defaultMessage: 'Number of matching documents is {thresholdComparator} {threshold}', - values: { - thresholdComparator: getHumanReadableComparator(comparator), - threshold: threshold.join(' and '), - }, - }); -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts similarity index 82% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index da2a459c046df..5ed782963e497 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -6,15 +6,14 @@ */ import { sha256 } from 'js-sha256'; import { i18n } from '@kbn/i18n'; -import { CoreSetup, Logger } from 'src/core/server'; -import { addMessages, EsQueryAlertActionContext } from '../action_context'; -import { ComparatorFns } from '../../lib'; -import { parseDuration } from '../../../../../alerting/server'; -import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from '../types'; -import { ActionGroupId, ConditionMetAlertInstanceId } from '../constants'; -import { getContextConditionsDescription, getInvalidComparatorError } from './messages'; +import { CoreSetup, Logger } from 'kibana/server'; +import { addMessages, EsQueryAlertActionContext } from './action_context'; +import { ComparatorFns, getHumanReadableComparator } from '../lib'; +import { parseDuration } from '../../../../alerting/server'; +import { ExecutorOptions, OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; +import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; import { fetchEsQuery } from './lib/fetch_es_query'; -import { EsQueryAlertParams } from '../alert_type_params'; +import { EsQueryAlertParams } from './alert_type_params'; import { fetchSearchSourceQuery } from './lib/fetch_search_source_query'; export async function executor( @@ -160,3 +159,22 @@ export function isEsQueryAlert(options: ExecutorOptions) { export function getChecksum(params: EsQueryAlertParams) { return sha256.create().update(JSON.stringify(params)); } + +export function getInvalidComparatorError(comparator: string) { + return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); +} + +export function getContextConditionsDescription(comparator: string, threshold: number[]) { + return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { + defaultMessage: 'Number of matching documents is {thresholdComparator} {threshold}', + values: { + thresholdComparator: getHumanReadableComparator(comparator), + threshold: threshold.join(' and '), + }, + }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts similarity index 91% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts index 917a5a33b1132..a7c9a53c4a7df 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts @@ -6,10 +6,10 @@ */ import { Logger } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { OnlyEsQueryAlertParams } from '../../types'; -import { IAbortableClusterClient } from '../../../../../../alerting/server'; -import { buildSortedEventsQuery } from '../../../../../common/build_sorted_events_query'; -import { ES_QUERY_ID } from '../../constants'; +import { OnlyEsQueryAlertParams } from '../types'; +import { IAbortableClusterClient } from '../../../../../alerting/server'; +import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query'; +import { ES_QUERY_ID } from '../constants'; import { getSearchParams } from './get_search_params'; export async function fetchEsQuery( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts similarity index 95% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_search_source_query.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts index 7ba279f36f553..695aef4651e74 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/fetch_search_source_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts @@ -6,8 +6,8 @@ */ import { buildRangeFilter, Filter } from '@kbn/es-query'; import { Logger } from 'kibana/server'; -import { OnlySearchSourceAlertParams } from '../../types'; -import { getTime, ISearchStartSearchSource } from '../../../../../../../../src/plugins/data/common'; +import { OnlySearchSourceAlertParams } from '../types'; +import { getTime, ISearchStartSearchSource } from '../../../../../../../src/plugins/data/common'; export async function fetchSearchSourceQuery( alertId: string, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/get_search_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts similarity index 92% rename from x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/get_search_params.ts rename to x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts index 8fa165523cba7..9a4d83630f21e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type/lib/get_search_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/get_search_params.ts @@ -5,8 +5,8 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { OnlyEsQueryAlertParams } from '../../types'; -import { parseDuration } from '../../../../../../alerting/common'; +import { OnlyEsQueryAlertParams } from '../types'; +import { parseDuration } from '../../../../../alerting/common'; export function getSearchParams(queryParams: OnlyEsQueryAlertParams) { const date = Date.now(); From 72698c13386d47048a23e8f8224c2754de126372 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 7 Mar 2022 11:59:06 +0100 Subject: [PATCH 38/67] Refactor, add tests + a bit more of documentation --- .../server/alert_types/es_query/executor.ts | 32 ++-- .../es_query/lib/fetch_es_query.ts | 11 +- .../lib/fetch_search_source_query.test.ts | 163 ++++++++++++++++++ .../es_query/lib/fetch_search_source_query.ts | 62 +++++-- 4 files changed, 231 insertions(+), 37 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index 5ed782963e497..5d57cd0fc9a22 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -25,24 +25,36 @@ export async function executor( const { alertId, name, services, params, state } = options; const { alertFactory, search, searchSourceClient } = services; const currentTimestamp = new Date().toISOString(); - const previousTimestamp = state.latestTimestamp; const publicBaseUrl = core.http.basePath.publicBaseUrl ?? ''; const compareFn = ComparatorFns.get(params.thresholdComparator); if (compareFn == null) { throw new Error(getInvalidComparatorError(params.thresholdComparator)); } - let timestamp: string | undefined = tryToParseAsDate(previousTimestamp); + let latestTimestamp: string | undefined = tryToParseAsDate(state.latestTimestamp); + + // During each alert execution, we run the configured query, get a hit count + // (hits.total) and retrieve up to params.size hits. We + // evaluate the threshold condition using the value of hits.total. If the threshold + // condition is met, the hits are counted toward the query match and we update + // the alert state with the timestamp of the latest hit. In the next execution + // of the alert, the latestTimestamp will be used to gate the query in order to + // avoid counting a document multiple times. const { numMatches, searchResult, dateStart, dateEnd } = esQueryAlert - ? await fetchEsQuery(alertId, name, params as OnlyEsQueryAlertParams, timestamp, { + ? await fetchEsQuery(alertId, name, params as OnlyEsQueryAlertParams, latestTimestamp, { search, logger, }) - : await fetchSearchSourceQuery(alertId, params as OnlySearchSourceAlertParams, timestamp, { - searchSourceClient, - logger, - }); + : await fetchSearchSourceQuery( + alertId, + params as OnlySearchSourceAlertParams, + latestTimestamp, + { + searchSourceClient, + logger, + } + ); // apply the alert condition const conditionMet = compareFn(numMatches, params.threshold); @@ -72,7 +84,7 @@ export async function executor( const alertInstance = alertFactory.create(ConditionMetAlertInstanceId); alertInstance // store the params we would need to recreate the query that led to this alert instance - .replaceState({ latestTimestamp: timestamp, dateStart, dateEnd }) + .replaceState({ latestTimestamp, dateStart, dateEnd }) .scheduleActions(ActionGroupId, actionContext); // update the timestamp based on the current search results @@ -80,11 +92,11 @@ export async function executor( searchResult.hits.hits.find((hit) => getValidTimefieldSort(hit.sort))?.sort ); if (firstValidTimefieldSort) { - timestamp = firstValidTimefieldSort; + latestTimestamp = firstValidTimefieldSort; } } - return { latestTimestamp: timestamp }; + return { latestTimestamp }; } function getInvalidWindowSizeError(windowValue: string) { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts index a7c9a53c4a7df..54dab17c2cc81 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_es_query.ts @@ -12,6 +12,9 @@ import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_q import { ES_QUERY_ID } from '../constants'; import { getSearchParams } from './get_search_params'; +/** + * Fetching matching documents for a given alert from elasticsearch by a given index and query + */ export async function fetchEsQuery( alertId: string, name: string, @@ -26,14 +29,6 @@ export async function fetchEsQuery( const abortableEsClient = search.asCurrentUser; const { parsedQuery, dateStart, dateEnd } = getSearchParams(params); - // During each alert execution, we run the configured query, get a hit count - // (hits.total) and retrieve up to params.size hits. We - // evaluate the threshold condition using the value of hits.total. If the threshold - // condition is met, the hits are counted toward the query match and we update - // the alert state with the timestamp of the latest hit. In the next execution - // of the alert, the latestTimestamp will be used to gate the query in order to - // avoid counting a document multiple times. - const filter = timestamp ? { bool: { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts new file mode 100644 index 0000000000000..e53ca9bcc767a --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts @@ -0,0 +1,163 @@ +/* + * 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 { OnlySearchSourceAlertParams } from '../types'; +import { createSearchSourceMock } from 'src/plugins/data/common/search/search_source/mocks'; +import { updateSearchSource } from './fetch_search_source_query'; +import { stubbedSavedObjectIndexPattern } from '../../../../../../../src/plugins/data_views/common/data_view.stub'; +import { DataView } from '../../../../../../../src/plugins/data_views/common'; +import { fieldFormatsMock } from '../../../../../../../src/plugins/field_formats/common/mocks'; + +const createDataView = () => { + const id = 'test-id'; + const { + type, + version, + attributes: { timeFieldName, fields, title }, + } = stubbedSavedObjectIndexPattern(id); + + return new DataView({ + spec: { id, type, version, timeFieldName, fields: JSON.parse(fields), title }, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: ['_id', '_type', '_score'], + }); +}; + +const defaultParams: OnlySearchSourceAlertParams = { + size: 100, + timeWindowSize: 5, + timeWindowUnit: 'm', + thresholdComparator: '<', + threshold: [0], + searchConfiguration: {}, + searchType: 'searchSource', +}; + +describe('fetchSearchSourceQuery', () => { + describe('updateSearchSource', () => { + const dataViewMock = createDataView(); + afterAll(() => { + jest.resetAllMocks(); + }); + + const fakeNow = new Date('2020-02-09T23:15:41.941Z'); + + beforeAll(() => { + jest.resetAllMocks(); + global.Date.now = jest.fn(() => fakeNow.getTime()); + }); + + it('without latest timestamp', async () => { + const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + + const searchSourceInstance = createSearchSourceMock({ index: dataViewMock }); + + const { searchSource, dateStart, dateEnd } = updateSearchSource( + searchSourceInstance, + params, + undefined + ); + const searchRequest = searchSource.getSearchRequestBody(); + expect(searchRequest.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "time": Object { + "format": "strict_date_optional_time", + "gte": "2020-02-09T23:10:41.941Z", + "lte": "2020-02-09T23:15:41.941Z", + }, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + } + `); + expect(dateStart).toMatch('2020-02-09T23:10:41.941Z'); + expect(dateEnd).toMatch('2020-02-09T23:15:41.941Z'); + }); + + it('with latest timestamp in between the given time range ', async () => { + const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + + const searchSourceInstance = createSearchSourceMock({ index: dataViewMock }); + + const { searchSource } = updateSearchSource( + searchSourceInstance, + params, + '2020-02-09T23:12:41.941Z' + ); + const searchRequest = searchSource.getSearchRequestBody(); + expect(searchRequest.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "time": Object { + "format": "strict_date_optional_time", + "gte": "2020-02-09T23:10:41.941Z", + "lte": "2020-02-09T23:15:41.941Z", + }, + }, + }, + Object { + "range": Object { + "time": Object { + "gt": "2020-02-09T23:12:41.941Z", + }, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + + it('with latest timestamp in before the given time range ', async () => { + const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + + const searchSourceInstance = createSearchSourceMock({ index: dataViewMock }); + + const { searchSource } = updateSearchSource( + searchSourceInstance, + params, + '2020-01-09T22:12:41.941Z' + ); + const searchRequest = searchSource.getSearchRequestBody(); + expect(searchRequest.query).toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "time": Object { + "format": "strict_date_optional_time", + "gte": "2020-02-09T23:10:41.941Z", + "lte": "2020-02-09T23:15:41.941Z", + }, + }, + }, + ], + "must": Array [], + "must_not": Array [], + "should": Array [], + }, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts index 695aef4651e74..f32c751b44742 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts @@ -7,12 +7,16 @@ import { buildRangeFilter, Filter } from '@kbn/es-query'; import { Logger } from 'kibana/server'; import { OnlySearchSourceAlertParams } from '../types'; -import { getTime, ISearchStartSearchSource } from '../../../../../../../src/plugins/data/common'; +import { + getTime, + ISearchSource, + ISearchStartSearchSource, +} from '../../../../../../../src/plugins/data/common'; export async function fetchSearchSourceQuery( alertId: string, params: OnlySearchSourceAlertParams, - timestamp: string | undefined, + latestTimestamp: string | undefined, services: { logger: Logger; searchSourceClient: Promise; @@ -20,15 +24,43 @@ export async function fetchSearchSourceQuery( ) { const { logger, searchSourceClient } = services; const client = await searchSourceClient; - const loadedSearchSource = await client.create(params.searchConfiguration); - const index = loadedSearchSource.getField('index'); + const initialSearchSource = await client.create(params.searchConfiguration); + + const { searchSource, dateStart, dateEnd } = updateSearchSource( + initialSearchSource, + params, + latestTimestamp + ); + + logger.debug( + `search source query alert (${alertId}) query: ${JSON.stringify( + searchSource.getSearchRequestBody() + )}` + ); + + const searchResult = await searchSource.fetch(); + + return { + numMatches: Number(searchResult.hits.total), + searchResult, + dateStart, + dateEnd, + }; +} + +export function updateSearchSource( + searchSource: ISearchSource, + params: OnlySearchSourceAlertParams, + latestTimestamp: string | undefined +) { + const index = searchSource.getField('index'); const timeFieldName = index?.timeFieldName; if (!timeFieldName) { throw new Error('Invalid data view without timeFieldName.'); } - loadedSearchSource.setField('size', params.size); + searchSource.setField('size', params.size); const timerangeFilter = getTime(index, { from: `now-${params.timeWindowSize}${params.timeWindowUnit}`, @@ -38,25 +70,17 @@ export async function fetchSearchSourceQuery( const dateEnd = timerangeFilter?.query.range[timeFieldName].lte; const filters = [timerangeFilter]; - if (timestamp) { + if (latestTimestamp && latestTimestamp > dateStart) { + // add additional filter for documents with a timestamp greater then + // the timestamp of the previous run, so that those documents are not counted twice const field = index.fields.find((f) => f.name === timeFieldName); - const addTimeRangeField = buildRangeFilter(field!, { gt: timestamp }, index); + const addTimeRangeField = buildRangeFilter(field!, { gt: latestTimestamp }, index); filters.push(addTimeRangeField); } - const searchSourceChild = loadedSearchSource.createChild(); + const searchSourceChild = searchSource.createChild(); searchSourceChild.setField('filter', filters as Filter[]); - - logger.debug( - `search source query alert (${alertId}) query: ${JSON.stringify( - searchSourceChild.getSearchRequestBody() - )}` - ); - - const searchResult = await searchSourceChild.fetch(); - return { - numMatches: Number(searchResult.hits.total), - searchResult, + searchSource: searchSourceChild, dateStart, dateEnd, }; From 2fc9d06b97c87bd4ab2dc397014f591c6d57fe4a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 7 Mar 2022 18:09:50 +0100 Subject: [PATCH 39/67] Move size field, change text --- .../expression/search_source_expression.tsx | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 5938ff947c328..cfc00fb572335 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -144,34 +144,9 @@ export const SearchSourceExpression = ({ - - - -
- -
-
- - { - setParam('size', updatedValue); - }} - /> -
@@ -210,7 +185,31 @@ export const SearchSourceExpression = ({ setParam('timeWindowUnit', selectedWindowUnit) } /> - + + +
+ +
+
+ + { + setParam('size', updatedValue); + }} + /> + + ); }; From db705ebeab73ce24ac114f67795eb3e953059210 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 7 Mar 2022 18:51:00 +0100 Subject: [PATCH 40/67] Implement readonly callout --- .../expression/search_source_expression.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index cfc00fb572335..24f33377db3e6 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -12,7 +12,6 @@ import { EuiSpacer, EuiTitle, EuiExpression, - EuiText, EuiLoadingSpinner, EuiEmptyPrompt, EuiCallOut, @@ -119,6 +118,17 @@ export const SearchSourceExpression = ({
+ + } + iconType="questionInCircle" + /> + - - - +
From 9c31f83e46a480d21c4a11d473ae6aac35630bda Mon Sep 17 00:00:00 2001 From: andreadelrio Date: Mon, 7 Mar 2022 14:56:18 -0800 Subject: [PATCH 41/67] change icon in callout --- .../es_query/expression/search_source_expression.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 24f33377db3e6..2e6336d1cfdaf 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -126,21 +126,19 @@ export const SearchSourceExpression = ({ defaultMessage="The data view, query, and filter are initialized in Discover cannot be edited subsequently." /> } - iconType="questionInCircle" + iconType="iInCircle" /> } - isActive={true} display="columns" /> From 6e2a817197b5ba266aa604f1a04c42071a6257a2 Mon Sep 17 00:00:00 2001 From: andreadelrio Date: Mon, 7 Mar 2022 15:45:34 -0800 Subject: [PATCH 42/67] add padding to popover --- src/plugins/data/public/ui/filter_bar/_global_filter_item.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 3df9fb6cf2d99..1c9cea7291770 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -91,4 +91,5 @@ .globalFilterItem__readonlyPanel { min-width: auto; + padding: $euiSizeM; } From f902a8a0ae7175cc7fd6ecc7b2d0c5f8e946dda4 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 8 Mar 2022 07:32:00 +0100 Subject: [PATCH 43/67] Hide query and filter UI if there are no values to display --- .../expression/search_source_expression.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 24f33377db3e6..dbde3a1148518 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -136,21 +136,25 @@ export const SearchSourceExpression = ({ isActive={true} display="columns" /> - - } - isActive={true} - display="columns" - /> + {query.query !== '' && ( + + )} + {filters.length > 0 && ( + } + isActive={true} + display="columns" + /> + )} From 184c26d73f94be9267ee685ce299ab988ba659bc Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 10 Mar 2022 12:43:37 +0500 Subject: [PATCH 44/67] [Discover] add unit test, improve comparator types --- .../stack_alerts/common/comparator_types.ts | 15 ++++ .../alert_types/es_query/alert_type.test.ts | 25 +++--- .../es_query/alert_type_params.test.ts | 3 +- .../alert_types/es_query/alert_type_params.ts | 22 ++--- .../alert_types/es_query/executor.test.ts | 80 +++++++++++++++++++ .../server/alert_types/es_query/executor.ts | 3 +- .../lib/fetch_search_source_query.test.ts | 9 ++- .../index_threshold/alert_type.test.ts | 11 +-- .../index_threshold/alert_type_params.test.ts | 3 +- .../index_threshold/alert_type_params.ts | 20 ++--- .../server/alert_types/lib/comparator.ts | 55 +++++++++++++ .../alert_types/lib/comparator_types.ts | 55 ------------- .../server/alert_types/lib/index.ts | 7 +- 13 files changed, 199 insertions(+), 109 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/common/comparator_types.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts create mode 100644 x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts delete mode 100644 x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts diff --git a/x-pack/plugins/stack_alerts/common/comparator_types.ts b/x-pack/plugins/stack_alerts/common/comparator_types.ts new file mode 100644 index 0000000000000..9e35d50f9158c --- /dev/null +++ b/x-pack/plugins/stack_alerts/common/comparator_types.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum Comparator { + GT = '>', + LT = '<', + GT_OR_EQ = '>=', + LT_OR_EQ = '<=', + BETWEEN = 'between', + NOT_BETWEEN = 'notBetween', +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index ccc4c244ec2c2..e7e4ac7a689ce 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -24,6 +24,7 @@ import { coreMock } from '../../../../../../src/core/server/mocks'; import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; import { OnlyEsQueryAlertParams, OnlySearchSourceAlertParams } from './types'; import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks'; +import { Comparator } from '../../../common/comparator_types'; const logger = loggingSystemMock.create().get(); const coreSetup = coreMock.createSetup(); @@ -111,7 +112,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [0], searchType: 'esQuery', }; @@ -130,7 +131,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: 'between', + thresholdComparator: Comparator.BETWEEN, threshold: [0], searchType: 'esQuery', }; @@ -148,7 +149,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: 'between', + thresholdComparator: Comparator.BETWEEN, threshold: [0], searchType: 'esQuery', }; @@ -178,7 +179,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], searchType: 'esQuery', }; @@ -224,7 +225,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], searchType: 'esQuery', }; @@ -273,7 +274,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], searchType: 'esQuery', }; @@ -316,7 +317,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], searchType: 'esQuery', }; @@ -388,7 +389,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], searchType: 'esQuery', }; @@ -434,7 +435,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], searchType: 'esQuery', }; @@ -502,7 +503,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [0], searchConfiguration: {}, searchType: 'searchSource', @@ -522,7 +523,7 @@ describe('alertType', () => { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [0], esQuery: '', searchType: 'searchSource', @@ -566,7 +567,7 @@ describe('alertType', () => { }); it('alert executor schedule actions when condition met', async () => { - const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); (searchSourceInstanceMock.getField as jest.Mock).mockImplementationOnce((name: string) => { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts index c0ad883271e9e..d1a14a7369e3b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts @@ -7,6 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import type { Writable } from '@kbn/utility-types'; +import { Comparator } from '../../../common/comparator_types'; import { EsQueryAlertParamsSchema, EsQueryAlertParams, @@ -20,7 +21,7 @@ const DefaultParams: Writable> = { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], searchType: 'esQuery', }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index 1b275938986e7..d892e70a7e7d9 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -6,10 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { schema, TypeOf } from '@kbn/config-schema'; -import { ComparatorFnNames } from '../lib'; +import { schema, Type, TypeOf } from '@kbn/config-schema'; import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server'; import { AlertTypeState } from '../../../../alerting/server'; +import { Comparator } from '../../../common/comparator_types'; +import { validateComparator } from '../lib'; export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000; @@ -19,12 +20,14 @@ export interface EsQueryAlertState extends AlertTypeState { latestTimestamp: string | undefined; } -export const EsQueryAlertParamsSchemaProperties = { +const EsQueryAlertParamsSchemaProperties = { size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), timeWindowSize: schema.number({ min: 1 }), timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), - thresholdComparator: schema.string({ validate: validateComparator }), + thresholdComparator: schema.string({ + validate: validateComparator('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage'), + }) as Type, searchType: schema.oneOf([schema.literal('esQuery'), schema.literal('searchSource')]), // searchSource alert param only searchConfiguration: schema.conditional( @@ -93,14 +96,3 @@ function validateParams(anyParams: unknown): string | undefined { }); } } - -export function validateComparator(comparator: string): string | undefined { - if (ComparatorFnNames.has(comparator)) return; - - return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator, - }, - }); -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts new file mode 100644 index 0000000000000..942f679b6d1a4 --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { getSearchParams, getValidTimefieldSort, tryToParseAsDate } from './executor'; +import { OnlyEsQueryAlertParams } from './types'; + +describe('es_query executor', () => { + const defaultProps = { + size: 3, + timeWindowSize: 5, + timeWindowUnit: 'm', + threshold: [], + thresholdComparator: '>=', + searchType: 'esQuery', + esQuery: '{ "query": "test-query" }', + index: ['test-index'], + timeField: '', + }; + describe('tryToParseAsDate', () => { + it('should parse as date correctly', () => { + const expectedResult = '2018-12-31T19:00:00.000Z'; + expect(expectedResult).toBe(tryToParseAsDate('2019-01-01T00:00:00')); + expect(expectedResult).toBe(tryToParseAsDate(1546282800000)); + }); + + it('should not parse as date', () => { + expect(undefined).toBe(tryToParseAsDate(null)); + expect(undefined).toBe(tryToParseAsDate(undefined)); + expect(undefined).toBe(tryToParseAsDate('invalid date')); + }); + }); + + describe('getValidTimefieldSort', () => { + it('should return valid time field', () => { + const result = getValidTimefieldSort([ + null, + 'invalid date', + '2018-12-31T19:00:00.000Z', + 1546282800000, + ]); + expect('2018-12-31T19:00:00.000Z').toEqual(result); + }); + }); + + describe('getSearchParams', () => { + it('should return search params correctly', () => { + const result = getSearchParams(defaultProps as OnlyEsQueryAlertParams); + expect('test-query').toBe(result.parsedQuery.query); + }); + + it('should throw invalid query error', () => { + expect(() => + getSearchParams({ ...defaultProps, esQuery: '' } as OnlyEsQueryAlertParams) + ).toThrow('invalid query specified: "" - query must be JSON'); + }); + + it('should throw invalid query error due to missing query property', () => { + expect(() => + getSearchParams({ + ...defaultProps, + esQuery: '{ "someProperty": "test-query" }', + } as OnlyEsQueryAlertParams) + ).toThrow('invalid query specified: "{ "someProperty": "test-query" }" - query must be JSON'); + }); + + it('should throw invalid window size error', () => { + expect(() => + getSearchParams({ + ...defaultProps, + timeWindowSize: 5, + timeWindowUnit: 'r', + } as OnlyEsQueryAlertParams) + ).toThrow('invalid format for windowSize: "5r"'); + }); + }); +}); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts index 5d57cd0fc9a22..1d78aae3b842a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.ts @@ -15,6 +15,7 @@ import { ActionGroupId, ConditionMetAlertInstanceId } from './constants'; import { fetchEsQuery } from './lib/fetch_es_query'; import { EsQueryAlertParams } from './alert_type_params'; import { fetchSearchSourceQuery } from './lib/fetch_search_source_query'; +import { Comparator } from '../../../common/comparator_types'; export async function executor( logger: Logger, @@ -181,7 +182,7 @@ export function getInvalidComparatorError(comparator: string) { }); } -export function getContextConditionsDescription(comparator: string, threshold: number[]) { +export function getContextConditionsDescription(comparator: Comparator, threshold: number[]) { return i18n.translate('xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription', { defaultMessage: 'Number of matching documents is {thresholdComparator} {threshold}', values: { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts index e53ca9bcc767a..1e6aac5dcb035 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.test.ts @@ -11,6 +11,7 @@ import { updateSearchSource } from './fetch_search_source_query'; import { stubbedSavedObjectIndexPattern } from '../../../../../../../src/plugins/data_views/common/data_view.stub'; import { DataView } from '../../../../../../../src/plugins/data_views/common'; import { fieldFormatsMock } from '../../../../../../../src/plugins/field_formats/common/mocks'; +import { Comparator } from '../../../../common/comparator_types'; const createDataView = () => { const id = 'test-id'; @@ -32,7 +33,7 @@ const defaultParams: OnlySearchSourceAlertParams = { size: 100, timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [0], searchConfiguration: {}, searchType: 'searchSource', @@ -53,7 +54,7 @@ describe('fetchSearchSourceQuery', () => { }); it('without latest timestamp', async () => { - const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] }; const searchSourceInstance = createSearchSourceMock({ index: dataViewMock }); @@ -88,7 +89,7 @@ describe('fetchSearchSourceQuery', () => { }); it('with latest timestamp in between the given time range ', async () => { - const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] }; const searchSourceInstance = createSearchSourceMock({ index: dataViewMock }); @@ -128,7 +129,7 @@ describe('fetchSearchSourceQuery', () => { }); it('with latest timestamp in before the given time range ', async () => { - const params = { ...defaultParams, thresholdComparator: '>=', threshold: [3] }; + const params = { ...defaultParams, thresholdComparator: Comparator.GT_OR_EQ, threshold: [3] }; const searchSourceInstance = createSearchSourceMock({ index: dataViewMock }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index e55ce6e3a3aba..060730fb668e3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -13,6 +13,7 @@ import { getAlertType, ActionGroupId } from './alert_type'; import { ActionContext } from './action_context'; import { Params } from './alert_type_params'; import { AlertServicesMock, alertsMock } from '../../../../alerting/server/mocks'; +import { Comparator } from '../../../common/comparator_types'; describe('alertType', () => { const logger = loggingSystemMock.create().get(); @@ -118,7 +119,7 @@ describe('alertType', () => { groupBy: 'all', timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [0], }; @@ -136,7 +137,7 @@ describe('alertType', () => { groupBy: 'all', timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], }; @@ -163,7 +164,7 @@ describe('alertType', () => { groupBy: 'all', timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [1], }; @@ -225,7 +226,7 @@ describe('alertType', () => { groupBy: 'all', timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [1], }; @@ -291,7 +292,7 @@ describe('alertType', () => { groupBy: 'all', timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '<', + thresholdComparator: Comparator.LT, threshold: [1], }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts index 65980601b67a8..a6533c494250f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.test.ts @@ -9,6 +9,7 @@ import { ParamsSchema, Params } from './alert_type_params'; import { ObjectType, TypeOf } from '@kbn/config-schema'; import type { Writable } from '@kbn/utility-types'; import { CoreQueryParams, MAX_GROUPS } from '../../../../triggers_actions_ui/server'; +import { Comparator } from '../../../common/comparator_types'; const DefaultParams: Writable> = { index: 'index-name', @@ -17,7 +18,7 @@ const DefaultParams: Writable> = { groupBy: 'all', timeWindowSize: 5, timeWindowUnit: 'm', - thresholdComparator: '>', + thresholdComparator: Comparator.GT, threshold: [0], }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts index d32e7890b17c6..dd34411fc5820 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts @@ -6,12 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { schema, TypeOf } from '@kbn/config-schema'; -import { ComparatorFnNames } from '../lib'; +import { schema, Type, TypeOf } from '@kbn/config-schema'; import { CoreQueryParamsSchemaProperties, validateCoreQueryBody, } from '../../../../triggers_actions_ui/server'; +import { validateComparator } from '../lib'; +import { Comparator } from '../../../common/comparator_types'; // alert type parameters @@ -21,7 +22,9 @@ export const ParamsSchema = schema.object( { ...CoreQueryParamsSchemaProperties, // the comparison function to use to determine if the threshold as been met - thresholdComparator: schema.string({ validate: validateComparator }), + thresholdComparator: schema.string({ + validate: validateComparator('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage'), + }) as Type, // the values to use as the threshold; `between` and `notBetween` require // two values, the others require one. threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), @@ -51,14 +54,3 @@ function validateParams(anyParams: unknown): string | undefined { }); } } - -export function validateComparator(comparator: string): string | undefined { - if (ComparatorFnNames.has(comparator)) return; - - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage', { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator, - }, - }); -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts new file mode 100644 index 0000000000000..49382ebcfdeed --- /dev/null +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { Comparator } from '../../../common/comparator_types'; + +export type ComparatorFn = (value: number, threshold: number[]) => boolean; + +const humanReadableComparators = new Map([ + [Comparator.LT, 'less than'], + [Comparator.LT_OR_EQ, 'less than or equal to'], + [Comparator.GT_OR_EQ, 'greater than or equal to'], + [Comparator.GT, 'greater than'], + [Comparator.BETWEEN, 'between'], + [Comparator.NOT_BETWEEN, 'not between'], +]); + +export const ComparatorFns = new Map([ + [Comparator.LT, (value: number, threshold: number[]) => value < threshold[0]], + [Comparator.LT_OR_EQ, (value: number, threshold: number[]) => value <= threshold[0]], + [Comparator.GT_OR_EQ, (value: number, threshold: number[]) => value >= threshold[0]], + [Comparator.GT, (value: number, threshold: number[]) => value > threshold[0]], + [ + Comparator.BETWEEN, + (value: number, threshold: number[]) => value >= threshold[0] && value <= threshold[1], + ], + [ + Comparator.NOT_BETWEEN, + (value: number, threshold: number[]) => value < threshold[0] || value > threshold[1], + ], +]); + +export const ComparatorFnNames = new Set(ComparatorFns.keys()); + +export function getHumanReadableComparator(comparator: Comparator) { + return humanReadableComparators.has(comparator) + ? humanReadableComparators.get(comparator) + : comparator; +} + +export const validateComparator = + (errorMessageId: string) => + (comparator: string): string | undefined => { + if (ComparatorFnNames.has(comparator as Comparator)) return; + + return i18n.translate(errorMessageId, { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); + }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts deleted file mode 100644 index b364a31b10151..0000000000000 --- a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator_types.ts +++ /dev/null @@ -1,55 +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. - */ - -enum Comparator { - GT = '>', - LT = '<', - GT_OR_EQ = '>=', - LT_OR_EQ = '<=', - BETWEEN = 'between', - NOT_BETWEEN = 'notBetween', -} - -const humanReadableComparators = new Map([ - [Comparator.LT, 'less than'], - [Comparator.LT_OR_EQ, 'less than or equal to'], - [Comparator.GT_OR_EQ, 'greater than or equal to'], - [Comparator.GT, 'greater than'], - [Comparator.BETWEEN, 'between'], - [Comparator.NOT_BETWEEN, 'not between'], -]); - -export const ComparatorFns = getComparatorFns(); -export const ComparatorFnNames = new Set(ComparatorFns.keys()); - -type ComparatorFn = (value: number, threshold: number[]) => boolean; - -function getComparatorFns(): Map { - const fns: Record = { - [Comparator.LT]: (value: number, threshold: number[]) => value < threshold[0], - [Comparator.LT_OR_EQ]: (value: number, threshold: number[]) => value <= threshold[0], - [Comparator.GT_OR_EQ]: (value: number, threshold: number[]) => value >= threshold[0], - [Comparator.GT]: (value: number, threshold: number[]) => value > threshold[0], - [Comparator.BETWEEN]: (value: number, threshold: number[]) => - value >= threshold[0] && value <= threshold[1], - [Comparator.NOT_BETWEEN]: (value: number, threshold: number[]) => - value < threshold[0] || value > threshold[1], - }; - - const result = new Map(); - for (const key of Object.keys(fns)) { - result.set(key, fns[key]); - } - - return result; -} - -export function getHumanReadableComparator(comparator: string) { - return humanReadableComparators.has(comparator) - ? humanReadableComparators.get(comparator) - : comparator; -} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts index 09219aad6fe5e..aadbdc884ce39 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts @@ -5,4 +5,9 @@ * 2.0. */ -export { ComparatorFns, ComparatorFnNames, getHumanReadableComparator } from './comparator_types'; +export { + ComparatorFns, + ComparatorFnNames, + getHumanReadableComparator, + validateComparator, +} from './comparator'; From 5dddc684914cfe9730d99984dbacfa1e2d5c52a2 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 10 Mar 2022 13:30:56 +0100 Subject: [PATCH 45/67] [Discover] fix linting and unit test --- .../alert_types/es_query/alert_type_params.ts | 17 +++++++++--- .../alert_types/es_query/executor.test.ts | 27 ++++++++++--------- .../index_threshold/alert_type_params.ts | 17 +++++++++--- .../server/alert_types/lib/comparator.ts | 15 +---------- .../server/alert_types/lib/index.ts | 7 +---- 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index d892e70a7e7d9..fa70c1fedd973 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -10,7 +10,7 @@ import { schema, Type, TypeOf } from '@kbn/config-schema'; import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server'; import { AlertTypeState } from '../../../../alerting/server'; import { Comparator } from '../../../common/comparator_types'; -import { validateComparator } from '../lib'; +import { ComparatorFnNames } from '../lib'; export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000; @@ -25,9 +25,7 @@ const EsQueryAlertParamsSchemaProperties = { timeWindowSize: schema.number({ min: 1 }), timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), - thresholdComparator: schema.string({ - validate: validateComparator('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage'), - }) as Type, + thresholdComparator: schema.string({ validate: validateComparator }) as Type, searchType: schema.oneOf([schema.literal('esQuery'), schema.literal('searchSource')]), // searchSource alert param only searchConfiguration: schema.conditional( @@ -96,3 +94,14 @@ function validateParams(anyParams: unknown): string | undefined { }); } } + +function validateComparator(comparator: string): string | undefined { + if (ComparatorFnNames.has(comparator as Comparator)) return; + + return i18n.translate('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts index 942f679b6d1a4..345d414ca908d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts @@ -21,17 +21,18 @@ describe('es_query executor', () => { timeField: '', }; describe('tryToParseAsDate', () => { - it('should parse as date correctly', () => { - const expectedResult = '2018-12-31T19:00:00.000Z'; - expect(expectedResult).toBe(tryToParseAsDate('2019-01-01T00:00:00')); - expect(expectedResult).toBe(tryToParseAsDate(1546282800000)); - }); - - it('should not parse as date', () => { - expect(undefined).toBe(tryToParseAsDate(null)); - expect(undefined).toBe(tryToParseAsDate(undefined)); - expect(undefined).toBe(tryToParseAsDate('invalid date')); - }); + it.each<[string | number]>([['2019-01-01T00:00:00.000Z'], [1546300800000]])( + 'should parse as date correctly', + (value) => { + expect(tryToParseAsDate(value)).toBe('2019-01-01T00:00:00.000Z'); + } + ); + it.each<[string | null | undefined]>([[null], ['invalid date'], [undefined]])( + 'should not parse as date', + (value) => { + expect(tryToParseAsDate(value)).toBe(undefined); + } + ); }); describe('getValidTimefieldSort', () => { @@ -42,14 +43,14 @@ describe('es_query executor', () => { '2018-12-31T19:00:00.000Z', 1546282800000, ]); - expect('2018-12-31T19:00:00.000Z').toEqual(result); + expect(result).toEqual('2018-12-31T19:00:00.000Z'); }); }); describe('getSearchParams', () => { it('should return search params correctly', () => { const result = getSearchParams(defaultProps as OnlyEsQueryAlertParams); - expect('test-query').toBe(result.parsedQuery.query); + expect(result.parsedQuery.query).toBe('test-query'); }); it('should throw invalid query error', () => { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts index dd34411fc5820..821ddce6fcfb1 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts @@ -11,7 +11,7 @@ import { CoreQueryParamsSchemaProperties, validateCoreQueryBody, } from '../../../../triggers_actions_ui/server'; -import { validateComparator } from '../lib'; +import { ComparatorFnNames } from '../lib'; import { Comparator } from '../../../common/comparator_types'; // alert type parameters @@ -22,9 +22,7 @@ export const ParamsSchema = schema.object( { ...CoreQueryParamsSchemaProperties, // the comparison function to use to determine if the threshold as been met - thresholdComparator: schema.string({ - validate: validateComparator('xpack.stackAlerts.esQuery.invalidComparatorErrorMessage'), - }) as Type, + thresholdComparator: schema.string({ validate: validateComparator }) as Type, // the values to use as the threshold; `between` and `notBetween` require // two values, the others require one. threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), @@ -54,3 +52,14 @@ function validateParams(anyParams: unknown): string | undefined { }); } } + +function validateComparator(comparator: string): string | undefined { + if (ComparatorFnNames.has(comparator as Comparator)) return; + + return i18n.translate('xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage', { + defaultMessage: 'invalid thresholdComparator specified: {comparator}', + values: { + comparator, + }, + }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts index 49382ebcfdeed..ac817256798a4 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/comparator.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; + import { Comparator } from '../../../common/comparator_types'; export type ComparatorFn = (value: number, threshold: number[]) => boolean; @@ -40,16 +40,3 @@ export function getHumanReadableComparator(comparator: Comparator) { ? humanReadableComparators.get(comparator) : comparator; } - -export const validateComparator = - (errorMessageId: string) => - (comparator: string): string | undefined => { - if (ComparatorFnNames.has(comparator as Comparator)) return; - - return i18n.translate(errorMessageId, { - defaultMessage: 'invalid thresholdComparator specified: {comparator}', - values: { - comparator, - }, - }); - }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts index aadbdc884ce39..7d2469defc91a 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/lib/index.ts @@ -5,9 +5,4 @@ * 2.0. */ -export { - ComparatorFns, - ComparatorFnNames, - getHumanReadableComparator, - validateComparator, -} from './comparator'; +export { ComparatorFns, ComparatorFnNames, getHumanReadableComparator } from './comparator'; From 9752dbc75167ba5bb0af14acd6249f1dab9cfcf2 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 14 Mar 2022 10:32:07 +0500 Subject: [PATCH 46/67] [Discover] add es query alert integration tests --- test/common/services/index_patterns.ts | 7 +- .../common/lib/es_test_index_tool.ts | 3 +- .../builtin_alert_types/es_query/alert.ts | 582 +++++++++++++----- .../spaces_only/tests/alerting/index.ts | 4 +- 4 files changed, 425 insertions(+), 171 deletions(-) diff --git a/test/common/services/index_patterns.ts b/test/common/services/index_patterns.ts index 549137c79e9a2..1e7e998ae24d9 100644 --- a/test/common/services/index_patterns.ts +++ b/test/common/services/index_patterns.ts @@ -16,13 +16,14 @@ export class IndexPatternsService extends FtrService { * Create a new index pattern */ async create( - indexPattern: { title: string }, - { override = false }: { override: boolean } = { override: false } + indexPattern: { title: string; timeFieldName?: string }, + { override = false }: { override: boolean }, + spaceId = '' ): Promise { const response = await this.kibanaServer.request<{ index_pattern: DataViewSpec; }>({ - path: '/api/index_patterns/index_pattern', + path: `${spaceId}/api/index_patterns/index_pattern`, method: 'POST', body: { override, diff --git a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts index c880ce945042f..524709e6c02a7 100644 --- a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts +++ b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts @@ -107,7 +107,8 @@ export class ESTestIndexTool { return await this.retry.try(async () => { const searchResult = await this.search(source, reference); // @ts-expect-error doesn't handle total: number - if (searchResult.body.hits.total.value < numDocs) { + const value = searchResult.body.hits.total.value?.value || searchResult.body.hits.total.value; + if (value < numDocs) { // @ts-expect-error doesn't handle total: number throw new Error(`Expected ${numDocs} but received ${searchResult.body.hits.total.value}.`); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 21c9774a02ac2..f6a419a6c947f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -32,6 +32,7 @@ const ES_GROUPS_TO_WRITE = 3; export default function alertTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); + const indexPatterns = getService('indexPatterns'); const es = getService('es'); const esTestIndexTool = new ESTestIndexTool(es, retry); const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); @@ -61,186 +62,428 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexToolOutput.destroy(); }); - it('runs correctly: threshold on hit count < >', async () => { - // write documents from now to the future end date in groups - createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - await createAlert({ - name: 'never fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '<', - threshold: [0], - searchType: 'esQuery', + describe('esQuery type', () => { + it('runs correctly: threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + + await createAlert({ + name: 'never fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '<', + threshold: [0], + searchType: 'esQuery', + }); + + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '>', + threshold: [-1], + searchType: 'esQuery', + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = + /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } else { + expect(previousTimestamp).not.to.be.empty(); + } + } }); - await createAlert({ - name: 'always fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '>', - threshold: [-1], - searchType: 'esQuery', + it('runs correctly: use epoch millis - threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + + await createAlert({ + name: 'never fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '<', + threshold: [0], + timeField: 'date_epoch_millis', + searchType: 'esQuery', + }); + + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '>', + threshold: [-1], + timeField: 'date_epoch_millis', + searchType: 'esQuery', + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = + /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } else { + expect(previousTimestamp).not.to.be.empty(); + } + } }); - const docs = await waitForDocs(2); - for (let i = 0; i < docs.length; i++) { - const doc = docs[i]; - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); - const messagePattern = - /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).not.to.be.empty(); - - // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution - if (!i) { + it('runs correctly with query: threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + + const rangeQuery = (rangeThreshold: number) => { + return { + query: { + bool: { + filter: [ + { + range: { + testedValue: { + gte: rangeThreshold, + }, + }, + }, + ], + }, + }, + }; + }; + + await createAlert({ + name: 'never fire', + esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), + size: 100, + thresholdComparator: '<', + threshold: [-1], + searchType: 'esQuery', + }); + + await createAlert({ + name: 'fires once', + esQuery: JSON.stringify( + rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) + ), + size: 100, + thresholdComparator: '>=', + threshold: [0], + searchType: 'esQuery', + }); + + const docs = await waitForDocs(1); + for (const doc of docs) { + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('fires once'); + expect(title).to.be(`alert 'fires once' matched query`); + const messagePattern = + /alert 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); expect(previousTimestamp).to.be.empty(); - } else { - expect(previousTimestamp).not.to.be.empty(); } - } - }); - - it('runs correctly: use epoch millis - threshold on hit count < >', async () => { - // write documents from now to the future end date in groups - createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - await createAlert({ - name: 'never fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '<', - threshold: [0], - timeField: 'date_epoch_millis', - searchType: 'esQuery', }); - await createAlert({ - name: 'always fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '>', - threshold: [-1], - timeField: 'date_epoch_millis', - searchType: 'esQuery', - }); - - const docs = await waitForDocs(2); - for (let i = 0; i < docs.length; i++) { - const doc = docs[i]; - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); - const messagePattern = - /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).not.to.be.empty(); - - // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution - if (!i) { - expect(previousTimestamp).to.be.empty(); - } else { - expect(previousTimestamp).not.to.be.empty(); + it('runs correctly: no matches', async () => { + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '<', + threshold: [1], + searchType: 'esQuery', + }); + + const docs = await waitForDocs(1); + + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = + /alert 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } else { + expect(previousTimestamp).not.to.be.empty(); + } } - } + }); }); - it('runs correctly with query: threshold on hit count < >', async () => { - // write documents from now to the future end date in groups - createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - const rangeQuery = (rangeThreshold: number) => { - return { - query: { - bool: { - filter: [ - { - range: { - testedValue: { - gte: rangeThreshold, - }, - }, - }, - ], + describe('searchSource type', () => { + it('runs correctly: threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + + await createAlert({ + name: 'never fire', + size: 100, + thresholdComparator: '<', + threshold: [0], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', }, + index: esTestDataView.id, + filter: [], }, - }; - }; - - await createAlert({ - name: 'never fire', - esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), - size: 100, - thresholdComparator: '<', - threshold: [-1], - searchType: 'esQuery', + }); + await createAlert({ + name: 'always fire', + size: 100, + thresholdComparator: '>', + threshold: [-1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = + /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + // expect(hits).not.to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } + // else { + // expect(previousTimestamp).not.to.be.empty(); + // } + } }); - await createAlert({ - name: 'fires once', - esQuery: JSON.stringify( - rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) - ), - size: 100, - thresholdComparator: '>=', - threshold: [0], - searchType: 'esQuery', + it('runs correctly: use epoch millis - threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date_epoch_millis' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + + await createAlert({ + name: 'never fire', + size: 100, + thresholdComparator: '<', + threshold: [0], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '>', + threshold: [-1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + + const docs = await waitForDocs(2); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = + /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } + // else { + // expect(previousTimestamp).not.to.be.empty(); + // } + } }); - const docs = await waitForDocs(1); - for (const doc of docs) { - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('fires once'); - expect(title).to.be(`alert 'fires once' matched query`); - const messagePattern = - /alert 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).not.to.be.empty(); - expect(previousTimestamp).to.be.empty(); - } - }); - - it('runs correctly: no matches', async () => { - await createAlert({ - name: 'always fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '<', - threshold: [1], - searchType: 'esQuery', + it('runs correctly with query: threshold on hit count < >', async () => { + // write documents from now to the future end date in groups + await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + + await createAlert({ + name: 'never fire', + size: 100, + thresholdComparator: '<', + threshold: [-1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: `testedValue > ${ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1}`, + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + + await createAlert({ + name: 'fires once', + size: 100, + thresholdComparator: '>=', + threshold: [0], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: `testedValue > ${Math.floor( + (ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2 + )}`, + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + + const docs = await waitForDocs(1); + for (const doc of docs) { + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('fires once'); + expect(title).to.be(`alert 'fires once' matched query`); + const messagePattern = + /alert 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).not.to.be.empty(); + expect(previousTimestamp).to.be.empty(); + } }); - const docs = await waitForDocs(1); - for (let i = 0; i < docs.length; i++) { - const doc = docs[i]; - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); - const messagePattern = - /alert 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).to.be.empty(); - - // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution - if (!i) { - expect(previousTimestamp).to.be.empty(); - } else { - expect(previousTimestamp).not.to.be.empty(); + it('runs correctly: no matches', async () => { + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + + await createAlert({ + name: 'always fire', + size: 100, + thresholdComparator: '<', + threshold: [1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + + const docs = await waitForDocs(1); + for (let i = 0; i < docs.length; i++) { + const doc = docs[i]; + const { previousTimestamp, hits } = doc._source; + const { name, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(title).to.be(`alert 'always fire' matched query`); + const messagePattern = + /alert 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; + expect(message).to.match(messagePattern); + expect(hits).to.be.empty(); + + // during the first execution, the latestTimestamp value should be empty + // since this alert always fires, the latestTimestamp value should be updated each execution + if (!i) { + expect(previousTimestamp).to.be.empty(); + } else { + expect(previousTimestamp).not.to.be.empty(); + } } - } + }); }); async function createEsDocumentsInGroups(groups: number) { @@ -264,12 +507,13 @@ export default function alertTests({ getService }: FtrProviderContext) { interface CreateAlertParams { name: string; - timeField?: string; - esQuery: string; size: number; thresholdComparator: string; threshold: number[]; timeWindowSize?: number; + esQuery?: string; + timeField?: string; + searchConfiguration?: unknown; searchType: 'esQuery' | 'searchSource'; } @@ -296,6 +540,16 @@ export default function alertTests({ getService }: FtrProviderContext) { }, }; + const alertParams = + params.searchType === 'esQuery' + ? { + index: [ES_TEST_INDEX_NAME], + timeField: params.timeField || 'date', + esQuery: params.esQuery, + } + : { + searchConfiguration: params.searchConfiguration, + }; const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') @@ -308,15 +562,13 @@ export default function alertTests({ getService }: FtrProviderContext) { actions: [action], notify_when: 'onActiveAlert', params: { - index: [ES_TEST_INDEX_NAME], - timeField: params.timeField || 'date', - esQuery: params.esQuery, size: params.size, timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5, timeWindowUnit: 's', thresholdComparator: params.thresholdComparator, threshold: params.threshold, searchType: params.searchType, + ...alertParams, }, }) .expect(200); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 242c6ffcba10f..6c9edc99b4d2d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -11,8 +11,8 @@ import { buildUp, tearDown } from '..'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { describe('Alerting', () => { - before(async () => buildUp(getService)); - after(async () => tearDown(getService)); + before(async () => await buildUp(getService)); + after(async () => await tearDown(getService)); loadTestFile(require.resolve('./aggregate')); loadTestFile(require.resolve('./create')); From ee029f2bcfe203a61a97b94b69b18152048a4d2e Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 14 Mar 2022 12:32:11 +0500 Subject: [PATCH 47/67] [Discover] fix linting --- .../tests/alerting/builtin_alert_types/es_query/alert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index f6a419a6c947f..67bddff7d7a1c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -292,7 +292,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const docs = await waitForDocs(2); for (let i = 0; i < docs.length; i++) { const doc = docs[i]; - const { previousTimestamp, hits } = doc._source; + const { previousTimestamp } = doc._source; const { name, title, message } = doc._source.params; expect(name).to.be('always fire'); From 07e02bf6c076b4f9b41d8642376ffe2d997b8c3c Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 14 Mar 2022 17:27:17 +0500 Subject: [PATCH 48/67] [Discover] uncomment one expect --- .../tests/alerting/builtin_alert_types/es_query/alert.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 67bddff7d7a1c..8e50b54202586 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -292,7 +292,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const docs = await waitForDocs(2); for (let i = 0; i < docs.length; i++) { const doc = docs[i]; - const { previousTimestamp } = doc._source; + const { previousTimestamp, hits } = doc._source; const { name, title, message } = doc._source.params; expect(name).to.be('always fire'); @@ -300,7 +300,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const messagePattern = /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; expect(message).to.match(messagePattern); - // expect(hits).not.to.be.empty(); + expect(hits).not.to.be.empty(); // during the first execution, the latestTimestamp value should be empty // since this alert always fires, the latestTimestamp value should be updated each execution From ddeb66da1a2c155bba5b6a5b63fe4db3223d2fbe Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 15 Mar 2022 12:07:03 +0500 Subject: [PATCH 49/67] [Discover] fix latesTimestamp for searchSource type, unify test logic --- .../es_query/lib/fetch_search_source_query.ts | 2 + .../builtin_alert_types/es_query/alert.ts | 597 ++++++++---------- 2 files changed, 268 insertions(+), 331 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts index f32c751b44742..490c6f81ec485 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/lib/fetch_search_source_query.ts @@ -11,6 +11,7 @@ import { getTime, ISearchSource, ISearchStartSearchSource, + SortDirection, } from '../../../../../../../src/plugins/data/common'; export async function fetchSearchSourceQuery( @@ -79,6 +80,7 @@ export function updateSearchSource( } const searchSourceChild = searchSource.createChild(); searchSourceChild.setField('filter', filters as Filter[]); + searchSourceChild.setField('sort', [{ [timeFieldName]: SortDirection.desc }]); return { searchSource: searchSourceChild, dateStart, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 8e50b54202586..83eb25be4afa2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -62,28 +62,73 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexToolOutput.destroy(); }); - describe('esQuery type', () => { - it('runs correctly: threshold on hit count < >', async () => { + [ + [ + 'esQuery', + async () => { + await createAlert({ + name: 'never fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '<', + threshold: [0], + searchType: 'esQuery', + }); + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '>', + threshold: [-1], + searchType: 'esQuery', + }); + }, + ] as const, + [ + 'searchSource', + async () => { + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + await createAlert({ + name: 'never fire', + size: 100, + thresholdComparator: '<', + threshold: [0], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + await createAlert({ + name: 'always fire', + size: 100, + thresholdComparator: '>', + threshold: [-1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + }, + ] as const, + ].forEach(([searchType, initData]) => + it(`runs correctly: threshold on hit count < > for ${searchType} search type`, async () => { // write documents from now to the future end date in groups createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - await createAlert({ - name: 'never fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '<', - threshold: [0], - searchType: 'esQuery', - }); - - await createAlert({ - name: 'always fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '>', - threshold: [-1], - searchType: 'esQuery', - }); + await initData(); const docs = await waitForDocs(2); for (let i = 0; i < docs.length; i++) { @@ -106,31 +151,78 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(previousTimestamp).not.to.be.empty(); } } - }); - - it('runs correctly: use epoch millis - threshold on hit count < >', async () => { + }) + ); + + [ + [ + 'esQuery', + async () => { + await createAlert({ + name: 'never fire', + size: 100, + thresholdComparator: '<', + threshold: [0], + timeField: 'date_epoch_millis', + searchType: 'esQuery', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + }); + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '>', + threshold: [-1], + timeField: 'date_epoch_millis', + searchType: 'esQuery', + }); + }, + ] as const, + [ + 'searchSource', + async () => { + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date_epoch_millis' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + await createAlert({ + name: 'never fire', + size: 100, + thresholdComparator: '<', + threshold: [0], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + await createAlert({ + name: 'always fire', + size: 100, + thresholdComparator: '>', + threshold: [-1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], + }, + }); + }, + ] as const, + ].forEach(([searchType, initData]) => + it(`runs correctly: use epoch millis - threshold on hit count < > for ${searchType} search type`, async () => { // write documents from now to the future end date in groups createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - await createAlert({ - name: 'never fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '<', - threshold: [0], - timeField: 'date_epoch_millis', - searchType: 'esQuery', - }); - - await createAlert({ - name: 'always fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '>', - threshold: [-1], - timeField: 'date_epoch_millis', - searchType: 'esQuery', - }); + await initData(); const docs = await waitForDocs(2); for (let i = 0; i < docs.length; i++) { @@ -153,276 +245,97 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(previousTimestamp).not.to.be.empty(); } } - }); - - it('runs correctly with query: threshold on hit count < >', async () => { - // write documents from now to the future end date in groups - createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - const rangeQuery = (rangeThreshold: number) => { - return { - query: { - bool: { - filter: [ - { - range: { - testedValue: { - gte: rangeThreshold, + }) + ); + + [ + [ + 'esQuery', + async () => { + const rangeQuery = (rangeThreshold: number) => { + return { + query: { + bool: { + filter: [ + { + range: { + testedValue: { + gte: rangeThreshold, + }, }, }, - }, - ], + ], + }, }, - }, + }; }; - }; - - await createAlert({ - name: 'never fire', - esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), - size: 100, - thresholdComparator: '<', - threshold: [-1], - searchType: 'esQuery', - }); - - await createAlert({ - name: 'fires once', - esQuery: JSON.stringify( - rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) - ), - size: 100, - thresholdComparator: '>=', - threshold: [0], - searchType: 'esQuery', - }); - - const docs = await waitForDocs(1); - for (const doc of docs) { - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('fires once'); - expect(title).to.be(`alert 'fires once' matched query`); - const messagePattern = - /alert 'fires once' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than or equal to 0 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).not.to.be.empty(); - expect(previousTimestamp).to.be.empty(); - } - }); - - it('runs correctly: no matches', async () => { - await createAlert({ - name: 'always fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '<', - threshold: [1], - searchType: 'esQuery', - }); - - const docs = await waitForDocs(1); - - for (let i = 0; i < docs.length; i++) { - const doc = docs[i]; - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); - const messagePattern = - /alert 'always fire' is active:\n\n- Value: 0+\n- Conditions Met: Number of matching documents is less than 1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).to.be.empty(); - - // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution - if (!i) { - expect(previousTimestamp).to.be.empty(); - } else { - expect(previousTimestamp).not.to.be.empty(); - } - } - }); - }); - - describe('searchSource type', () => { - it('runs correctly: threshold on hit count < >', async () => { - // write documents from now to the future end date in groups - await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - const esTestDataView = await indexPatterns.create( - { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, - { override: true }, - getUrlPrefix(Spaces.space1.id) - ); - - await createAlert({ - name: 'never fire', - size: 100, - thresholdComparator: '<', - threshold: [0], - searchType: 'searchSource', - searchConfiguration: { - query: { - query: '', - language: 'kuery', - }, - index: esTestDataView.id, - filter: [], - }, - }); - await createAlert({ - name: 'always fire', - size: 100, - thresholdComparator: '>', - threshold: [-1], - searchType: 'searchSource', - searchConfiguration: { - query: { - query: '', - language: 'kuery', - }, - index: esTestDataView.id, - filter: [], - }, - }); - - const docs = await waitForDocs(2); - for (let i = 0; i < docs.length; i++) { - const doc = docs[i]; - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); - const messagePattern = - /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).not.to.be.empty(); - - // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution - if (!i) { - expect(previousTimestamp).to.be.empty(); - } - // else { - // expect(previousTimestamp).not.to.be.empty(); - // } - } - }); - - it('runs correctly: use epoch millis - threshold on hit count < >', async () => { - // write documents from now to the future end date in groups - await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - const esTestDataView = await indexPatterns.create( - { title: ES_TEST_INDEX_NAME, timeFieldName: 'date_epoch_millis' }, - { override: true }, - getUrlPrefix(Spaces.space1.id) - ); - - await createAlert({ - name: 'never fire', - size: 100, - thresholdComparator: '<', - threshold: [0], - searchType: 'searchSource', - searchConfiguration: { - query: { - query: '', - language: 'kuery', + await createAlert({ + name: 'never fire', + esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), + size: 100, + thresholdComparator: '<', + threshold: [-1], + searchType: 'esQuery', + }); + await createAlert({ + name: 'fires once', + esQuery: JSON.stringify( + rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) + ), + size: 100, + thresholdComparator: '>=', + threshold: [0], + searchType: 'esQuery', + }); + }, + ] as const, + [ + 'searchSource', + async () => { + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + await createAlert({ + name: 'never fire', + size: 100, + thresholdComparator: '<', + threshold: [-1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: `testedValue > ${ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1}`, + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], }, - index: esTestDataView.id, - filter: [], - }, - }); - - await createAlert({ - name: 'always fire', - esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - size: 100, - thresholdComparator: '>', - threshold: [-1], - searchType: 'searchSource', - searchConfiguration: { - query: { - query: '', - language: 'kuery', + }); + await createAlert({ + name: 'fires once', + size: 100, + thresholdComparator: '>=', + threshold: [0], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: `testedValue > ${Math.floor( + (ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2 + )}`, + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], }, - index: esTestDataView.id, - filter: [], - }, - }); - - const docs = await waitForDocs(2); - for (let i = 0; i < docs.length; i++) { - const doc = docs[i]; - const { previousTimestamp, hits } = doc._source; - const { name, title, message } = doc._source.params; - - expect(name).to.be('always fire'); - expect(title).to.be(`alert 'always fire' matched query`); - const messagePattern = - /alert 'always fire' is active:\n\n- Value: \d+\n- Conditions Met: Number of matching documents is greater than -1 over 15s\n- Timestamp: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; - expect(message).to.match(messagePattern); - expect(hits).not.to.be.empty(); - - // during the first execution, the latestTimestamp value should be empty - // since this alert always fires, the latestTimestamp value should be updated each execution - if (!i) { - expect(previousTimestamp).to.be.empty(); - } - // else { - // expect(previousTimestamp).not.to.be.empty(); - // } - } - }); - - it('runs correctly with query: threshold on hit count < >', async () => { + }); + }, + ] as const, + ].forEach(([searchType, initData]) => + it(`runs correctly with query: threshold on hit count < > for ${searchType}`, async () => { // write documents from now to the future end date in groups - await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); - - const esTestDataView = await indexPatterns.create( - { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, - { override: true }, - getUrlPrefix(Spaces.space1.id) - ); - - await createAlert({ - name: 'never fire', - size: 100, - thresholdComparator: '<', - threshold: [-1], - searchType: 'searchSource', - searchConfiguration: { - query: { - query: `testedValue > ${ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1}`, - language: 'kuery', - }, - index: esTestDataView.id, - filter: [], - }, - }); - - await createAlert({ - name: 'fires once', - size: 100, - thresholdComparator: '>=', - threshold: [0], - searchType: 'searchSource', - searchConfiguration: { - query: { - query: `testedValue > ${Math.floor( - (ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2 - )}`, - language: 'kuery', - }, - index: esTestDataView.id, - filter: [], - }, - }); + createEsDocumentsInGroups(ES_GROUPS_TO_WRITE); + await initData(); const docs = await waitForDocs(1); for (const doc of docs) { @@ -437,30 +350,52 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(hits).not.to.be.empty(); expect(previousTimestamp).to.be.empty(); } - }); - - it('runs correctly: no matches', async () => { - const esTestDataView = await indexPatterns.create( - { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, - { override: true }, - getUrlPrefix(Spaces.space1.id) - ); - - await createAlert({ - name: 'always fire', - size: 100, - thresholdComparator: '<', - threshold: [1], - searchType: 'searchSource', - searchConfiguration: { - query: { - query: '', - language: 'kuery', + }) + ); + + [ + [ + 'esQuery', + async () => { + await createAlert({ + name: 'always fire', + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, + thresholdComparator: '<', + threshold: [1], + searchType: 'esQuery', + }); + }, + ] as const, + [ + 'searchSource', + async () => { + const esTestDataView = await indexPatterns.create( + { title: ES_TEST_INDEX_NAME, timeFieldName: 'date' }, + { override: true }, + getUrlPrefix(Spaces.space1.id) + ); + + await createAlert({ + name: 'always fire', + size: 100, + thresholdComparator: '<', + threshold: [1], + searchType: 'searchSource', + searchConfiguration: { + query: { + query: '', + language: 'kuery', + }, + index: esTestDataView.id, + filter: [], }, - index: esTestDataView.id, - filter: [], - }, - }); + }); + }, + ] as const, + ].forEach(([searchType, initData]) => + it(`runs correctly: no matches for ${searchType} search type`, async () => { + await initData(); const docs = await waitForDocs(1); for (let i = 0; i < docs.length; i++) { @@ -483,8 +418,8 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(previousTimestamp).not.to.be.empty(); } } - }); - }); + }) + ); async function createEsDocumentsInGroups(groups: number) { await createEsDocuments( From 8333c8f17b8a59e2a8ecfc3c7ca5d16a1f0165ff Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Thu, 17 Mar 2022 12:57:17 +0500 Subject: [PATCH 50/67] Update x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- .../es_query/expression/search_source_expression.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 884bb0421064c..1962bb576311b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -123,7 +123,7 @@ export const SearchSourceExpression = ({ title={ } iconType="iInCircle" From 6bcded6812151202dbd7b609fe8a76c251f9d2c2 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 17 Mar 2022 14:38:38 +0500 Subject: [PATCH 51/67] [Discover] apply suggestions --- .../public/application/view_alert/view_alert_route.tsx | 4 ++-- .../es_query/expression/search_source_expression.tsx | 2 +- .../stack_alerts/public/alert_types/es_query/index.ts | 3 ++- .../server/alert_types/es_query/action_context.ts | 7 ++++++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 2f3e6f5b5fcff..86d5f4b3fb089 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -45,8 +45,8 @@ const displayRuleChangedWarn = (toastNotifications: ToastsStart) => { defaultMessage: 'Alert rule has changed', }); const warnDescription = i18n.translate('discover.viewAlert.alertRuleChangedWarnDescription', { - defaultMessage: `Displayed documents might not match the documents triggered notification, - since the rule configuration has been changed.`, + defaultMessage: `The displayed documents might not match the documents that triggered the alert + because the rule configuration changed.`, }); toastNotifications.addWarning({ diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 1962bb576311b..1d0406090e382 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -113,7 +113,7 @@ export const SearchSourceExpression = ({
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts index 795824389caf8..218cbff3bb1ec 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts @@ -35,7 +35,8 @@ export function getAlertType(alerting: AlertingSetup): RuleTypeModel Date: Thu, 17 Mar 2022 22:16:15 +0500 Subject: [PATCH 52/67] [Discover] make searchType optional, adjust tests --- .../expression/es_query_expression.test.tsx | 1 - .../expression/es_query_expression.tsx | 1 - .../public/alert_types/es_query/types.ts | 8 +++---- .../alert_types/es_query/validation.test.ts | 9 -------- .../es_query/action_context.test.ts | 10 ++++---- .../alert_types/es_query/alert_type.test.ts | 13 ++--------- .../server/alert_types/es_query/alert_type.ts | 4 ++-- .../es_query/alert_type_params.test.ts | 1 - .../alert_types/es_query/alert_type_params.ts | 23 +++++++++---------- .../alert_types/es_query/executor.test.ts | 1 - .../server/alert_types/es_query/types.ts | 4 +--- .../builtin_alert_types/es_query/alert.ts | 18 +++++---------- 12 files changed, 32 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx index 5f1fd2fda071f..3cddd1ef112f0 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx @@ -109,7 +109,6 @@ const defaultEsQueryExpressionParams: EsQueryAlertParams = { index: ['test-index'], timeField: '@timestamp', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, - searchType: SearchType.esQuery, }; describe('EsQueryAlertTypeExpression', () => { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx index 6d8bc982f7817..45d7791055f87 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx @@ -81,7 +81,6 @@ export const EsQueryExpression = ({ threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR, size: size ?? DEFAULT_VALUES.SIZE, - searchType: SearchType.esQuery, esQuery: esQuery ?? DEFAULT_VALUES.QUERY, }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts index 702dbfa5d2a3a..b60183d7ae2fb 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts @@ -25,12 +25,11 @@ export interface CommonAlertParams extends AlertTypeParams threshold: number[]; timeWindowSize: number; timeWindowUnit: string; - searchType: T; } -export type EsQueryAlertParams = T extends SearchType.esQuery - ? CommonAlertParams & OnlyEsQueryAlertParams - : CommonAlertParams & OnlySearchSourceAlertParams; +export type EsQueryAlertParams = T extends SearchType.searchSource + ? CommonAlertParams & OnlySearchSourceAlertParams + : CommonAlertParams & OnlyEsQueryAlertParams; export interface OnlyEsQueryAlertParams { esQuery: string; @@ -38,5 +37,6 @@ export interface OnlyEsQueryAlertParams { timeField: string; } export interface OnlySearchSourceAlertParams { + searchType: 'searchSource'; searchConfiguration: SerializedSearchSourceFields; } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts index 5b0f76042abb2..90b7f96b781b9 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts @@ -18,7 +18,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', threshold: [0], timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.index.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.index[0]).toBe('Index is required.'); @@ -33,7 +32,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', threshold: [0], timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.timeField.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.timeField[0]).toBe('Time field is required.'); @@ -48,7 +46,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', threshold: [0], timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.esQuery[0]).toBe('Query must be valid JSON.'); @@ -63,7 +60,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', threshold: [0], timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.esQuery[0]).toBe(`Query field is required.`); @@ -93,7 +89,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', thresholdComparator: '<', timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.threshold0.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.threshold0[0]).toBe('Threshold 0 is required.'); @@ -109,7 +104,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', thresholdComparator: 'between', timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.threshold1[0]).toBe('Threshold 1 is required.'); @@ -125,7 +119,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', thresholdComparator: 'between', timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.threshold1[0]).toBe( @@ -142,7 +135,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', threshold: [0], timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.size[0]).toBe( @@ -159,7 +151,6 @@ describe('expression params validation', () => { timeWindowUnit: 's', threshold: [0], timeField: '', - searchType: SearchType.esQuery, }; expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); expect(validateExpression(initialParams).errors.size[0]).toBe( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts index f9b5c1c692126..468729fb2120d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -20,13 +20,13 @@ describe('ActionContext', () => { timeWindowUnit: 'm', thresholdComparator: '>', threshold: [4], - searchType: 'esQuery', }) as OnlyEsQueryAlertParams; const base: EsQueryAlertActionContext = { date: '2020-01-01T00:00:00.000Z', value: 42, conditions: 'count greater than 4', hits: [], + link: 'link-mock', }; const context = addMessages({ name: '[alert-name]' }, base, params); expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`); @@ -35,7 +35,8 @@ describe('ActionContext', () => { - Value: 42 - Conditions Met: count greater than 4 over 5m -- Timestamp: 2020-01-01T00:00:00.000Z` +- Timestamp: 2020-01-01T00:00:00.000Z +- Link: link-mock` ); }); @@ -49,13 +50,13 @@ describe('ActionContext', () => { timeWindowUnit: 'm', thresholdComparator: 'between', threshold: [4, 5], - searchType: 'esQuery', }) as OnlyEsQueryAlertParams; const base: EsQueryAlertActionContext = { date: '2020-01-01T00:00:00.000Z', value: 4, conditions: 'count between 4 and 5', hits: [], + link: 'link-mock', }; const context = addMessages({ name: '[alert-name]' }, base, params); expect(context.title).toMatchInlineSnapshot(`"alert '[alert-name]' matched query"`); @@ -64,7 +65,8 @@ describe('ActionContext', () => { - Value: 4 - Conditions Met: count between 4 and 5 over 5m -- Timestamp: 2020-01-01T00:00:00.000Z` +- Timestamp: 2020-01-01T00:00:00.000Z +- Link: link-mock` ); }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 7a3d7e78c52b8..065c85fa1a17d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -64,8 +64,8 @@ describe('alertType', () => { "name": "conditions", }, Object { - "description": "A link to the records that triggered this alert, if it was created from Discover. - For Elastic query alerts, this link navigates to Stack Management.", + "description": "If the alert rule was created in Discover, the link will navigate + to Discover showing the records that triggered this alert. In the other case the link will navigate to the rule's status page.", "name": "link", }, ], @@ -114,7 +114,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.LT, threshold: [0], - searchType: 'esQuery', }; expect(alertType.validate?.params?.validate(params)).toBeTruthy(); @@ -133,7 +132,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.BETWEEN, threshold: [0], - searchType: 'esQuery', }; expect(() => paramsSchema.validate(params)).toThrowErrorMatchingInlineSnapshot( @@ -151,7 +149,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.BETWEEN, threshold: [0], - searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -181,7 +178,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], - searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -227,7 +223,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], - searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -276,7 +271,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], - searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -319,7 +313,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], - searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -391,7 +384,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], - searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); @@ -437,7 +429,6 @@ describe('alertType', () => { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], - searchType: 'esQuery', }; const alertServices: AlertServicesMock = alertsMock.createAlertServices(); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 3f904b9142555..e0ae36bb5d66f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -134,8 +134,8 @@ export function getAlertType( const actionVariableContextLinkLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', { - defaultMessage: `A link to the records that triggered this alert, if it was created from Discover. - For Elastic query alerts, this link navigates to Stack Management.`, + defaultMessage: `If the alert rule was created in Discover, the link will navigate + to Discover showing the records that triggered this alert. In the other case the link will navigate to the rule's status page.`, } ); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts index d1a14a7369e3b..62833725bba1c 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts @@ -23,7 +23,6 @@ const DefaultParams: Writable> = { timeWindowUnit: 'm', thresholdComparator: Comparator.GT, threshold: [0], - searchType: 'esQuery', }; describe('alertType Params validate()', () => { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index fa70c1fedd973..79618976d3aa0 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -26,7 +26,7 @@ const EsQueryAlertParamsSchemaProperties = { timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), thresholdComparator: schema.string({ validate: validateComparator }) as Type, - searchType: schema.oneOf([schema.literal('esQuery'), schema.literal('searchSource')]), + searchType: schema.nullable(schema.literal('searchSource')), // searchSource alert param only searchConfiguration: schema.conditional( schema.siblingRef('searchType'), @@ -37,21 +37,21 @@ const EsQueryAlertParamsSchemaProperties = { // esQuery alert params only esQuery: schema.conditional( schema.siblingRef('searchType'), - schema.literal('esQuery'), - schema.string({ minLength: 1 }), - schema.never() + schema.literal('searchSource'), + schema.never(), + schema.string({ minLength: 1 }) ), index: schema.conditional( schema.siblingRef('searchType'), - schema.literal('esQuery'), - schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), - schema.never() + schema.literal('searchSource'), + schema.never(), + schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }) ), timeField: schema.conditional( schema.siblingRef('searchType'), - schema.literal('esQuery'), - schema.string({ minLength: 1 }), - schema.never() + schema.literal('searchSource'), + schema.never(), + schema.string({ minLength: 1 }) ), }; @@ -63,8 +63,7 @@ const betweenComparators = new Set(['between', 'notBetween']); // using direct type not allowed, circular reference, so body is typed to any function validateParams(anyParams: unknown): string | undefined { - const { esQuery, thresholdComparator, threshold, searchType }: EsQueryAlertParams = - anyParams as EsQueryAlertParams; + const { esQuery, thresholdComparator, threshold, searchType } = anyParams as EsQueryAlertParams; if (betweenComparators.has(thresholdComparator) && threshold.length === 1) { return i18n.translate('xpack.stackAlerts.esQuery.invalidThreshold2ErrorMessage', { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts index 345d414ca908d..670f76f5e19de 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/executor.test.ts @@ -15,7 +15,6 @@ describe('es_query executor', () => { timeWindowUnit: 'm', threshold: [], thresholdComparator: '>=', - searchType: 'esQuery', esQuery: '{ "query": "test-query" }', index: ['test-index'], timeField: '', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts index 512ac3d8cd359..3bcfbc1aef8f9 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/types.ts @@ -10,9 +10,7 @@ import { ActionContext } from './action_context'; import { EsQueryAlertParams, EsQueryAlertState } from './alert_type_params'; import { ActionGroupId } from './constants'; -export type OnlyEsQueryAlertParams = Omit & { - searchType: 'esQuery'; -}; +export type OnlyEsQueryAlertParams = Omit; export type OnlySearchSourceAlertParams = Omit< EsQueryAlertParams, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 83eb25be4afa2..e62ad1db4a652 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -72,7 +72,6 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '<', threshold: [0], - searchType: 'esQuery', }); await createAlert({ name: 'always fire', @@ -80,7 +79,6 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '>', threshold: [-1], - searchType: 'esQuery', }); }, ] as const, @@ -164,7 +162,6 @@ export default function alertTests({ getService }: FtrProviderContext) { thresholdComparator: '<', threshold: [0], timeField: 'date_epoch_millis', - searchType: 'esQuery', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, }); await createAlert({ @@ -174,7 +171,6 @@ export default function alertTests({ getService }: FtrProviderContext) { thresholdComparator: '>', threshold: [-1], timeField: 'date_epoch_millis', - searchType: 'esQuery', }); }, ] as const, @@ -275,7 +271,6 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '<', threshold: [-1], - searchType: 'esQuery', }); await createAlert({ name: 'fires once', @@ -285,7 +280,6 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '>=', threshold: [0], - searchType: 'esQuery', }); }, ] as const, @@ -363,7 +357,6 @@ export default function alertTests({ getService }: FtrProviderContext) { size: 100, thresholdComparator: '<', threshold: [1], - searchType: 'esQuery', }); }, ] as const, @@ -449,7 +442,7 @@ export default function alertTests({ getService }: FtrProviderContext) { esQuery?: string; timeField?: string; searchConfiguration?: unknown; - searchType: 'esQuery' | 'searchSource'; + searchType?: 'searchSource'; } async function createAlert(params: CreateAlertParams): Promise { @@ -476,15 +469,16 @@ export default function alertTests({ getService }: FtrProviderContext) { }; const alertParams = - params.searchType === 'esQuery' + params.searchType === 'searchSource' ? { + searchConfiguration: params.searchConfiguration, + } + : { index: [ES_TEST_INDEX_NAME], timeField: params.timeField || 'date', esQuery: params.esQuery, - } - : { - searchConfiguration: params.searchConfiguration, }; + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') From 97a622e9294a4ff532788b5c9d14598e1151b3d5 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 18 Mar 2022 09:32:48 +0500 Subject: [PATCH 53/67] [Discover] remove updated translations --- x-pack/plugins/translations/translations/fr-FR.json | 1 - x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 3 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index ea47bedb67152..117631ad4d965 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -23413,7 +23413,6 @@ "xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "Titre pour l'alerte.", "xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "Valeur ayant rempli la condition de seuil.", "xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "Le nombre de documents correspondants est {thresholdComparator} {threshold}", - "xpack.stackAlerts.esQuery.alertTypeContextMessageDescription": "l'alerte \"{name}\" est active :\n\n- Valeur : {value}\n- Conditions remplies : {conditions} sur {window}\n- Horodatage : {date}", "xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "l'alerte \"{name}\" correspond à la recherche", "xpack.stackAlerts.esQuery.alertTypeTitle": "Recherche Elasticsearch", "xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "thresholdComparator spécifié non valide : {comparator}", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8f96343daced1..467e0b4d81849 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26781,7 +26781,6 @@ "xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "アラートのタイトル。", "xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "しきい値条件を満たした値。", "xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "一致するドキュメント数は{thresholdComparator} {threshold}です", - "xpack.stackAlerts.esQuery.alertTypeContextMessageDescription": "アラート'{name}'は有効です。\n\n- 値:{value}\n- 条件が満たされました:{window} の {conditions}\n- タイムスタンプ:{date}", "xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "アラート'{name}'はクエリと一致しました", "xpack.stackAlerts.esQuery.alertTypeTitle": "Elasticsearch クエリ", "xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "無効な thresholdComparator が指定されました:{comparator}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f49f943fbd2e3..3a103993cf8f3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26812,7 +26812,6 @@ "xpack.stackAlerts.esQuery.actionVariableContextTitleLabel": "告警的标题。", "xpack.stackAlerts.esQuery.actionVariableContextValueLabel": "满足阈值条件的值。", "xpack.stackAlerts.esQuery.alertTypeContextConditionsDescription": "匹配文档的数目{thresholdComparator} {threshold}", - "xpack.stackAlerts.esQuery.alertTypeContextMessageDescription": "告警“{name}”处于活动状态:\n\n- 值:{value}\n- 满足的条件:{conditions} 超过 {window}\n- 时间戳:{date}", "xpack.stackAlerts.esQuery.alertTypeContextSubjectTitle": "告警“{name}”已匹配查询", "xpack.stackAlerts.esQuery.alertTypeTitle": "Elasticsearch 查询", "xpack.stackAlerts.esQuery.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", From b6a39e98c67f50799f1207a89a1b842275c4987b Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 18 Mar 2022 09:41:01 +0500 Subject: [PATCH 54/67] [Discover] apply suggestions --- .../application/view_alert/view_alert_route.tsx | 6 +++--- .../server/alert_types/es_query/alert_type.ts | 12 ++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 86d5f4b3fb089..56556384a8a6f 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -104,7 +104,7 @@ export function ViewAlertRoute() { ); } catch (error) { const errorTitle = i18n.translate('discover.viewAlert.alertRuleFetchErrorTitle', { - defaultMessage: 'Alert rule fetch error', + defaultMessage: 'Error fetching alert rule', }); displayError(errorTitle, error.message, toastNotifications); } @@ -115,7 +115,7 @@ export function ViewAlertRoute() { return await data.search.searchSource.create(fetchedAlert.params.searchConfiguration); } catch (error) { const errorTitle = i18n.translate('discover.viewAlert.searchSourceErrorTitle', { - defaultMessage: 'Search source fetch error', + defaultMessage: 'Error fetching search source', }); displayError(errorTitle, error.message, toastNotifications); } @@ -123,7 +123,7 @@ export function ViewAlertRoute() { const showDataViewFetchError = (alertId: string) => { const errorTitle = i18n.translate('discover.viewAlert.dataViewErrorTitle', { - defaultMessage: 'Data view fetch error', + defaultMessage: 'Error fetching data view', }); displayError( errorTitle, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index e0ae36bb5d66f..86732a6803ee7 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -116,13 +116,6 @@ export function getAlertType( } ); - const actionVariableSearchTypeLabel = i18n.translate( - 'xpack.stackAlerts.esQuery.actionVariableContextSearchTypeLabel', - { - defaultMessage: 'The type of search.', - } - ); - const actionVariableSearchConfigurationLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextSearchConfigurationLabel', { @@ -134,8 +127,8 @@ export function getAlertType( const actionVariableContextLinkLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextLinkLabel', { - defaultMessage: `If the alert rule was created in Discover, the link will navigate - to Discover showing the records that triggered this alert. In the other case the link will navigate to the rule's status page.`, + defaultMessage: `Navigate to Discover and show the records that triggered + the alert when the rule is created in Discover. Otherwise, navigate to the status page for the rule.`, } ); @@ -161,7 +154,6 @@ export function getAlertType( { name: 'size', description: actionVariableContextSizeLabel }, { name: 'threshold', description: actionVariableContextThresholdLabel }, { name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel }, - { name: 'searchType', description: actionVariableSearchTypeLabel }, { name: 'searchConfiguration', description: actionVariableSearchConfigurationLabel }, { name: 'esQuery', description: actionVariableContextQueryLabel }, { name: 'index', description: actionVariableContextIndexLabel }, From 84112fca65616bd7ace206d979968a064ca5d4ea Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 18 Mar 2022 10:59:41 +0500 Subject: [PATCH 55/67] [Discover] fix unit test --- .../server/alert_types/es_query/alert_type.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 065c85fa1a17d..18f182d32deb2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -64,8 +64,8 @@ describe('alertType', () => { "name": "conditions", }, Object { - "description": "If the alert rule was created in Discover, the link will navigate - to Discover showing the records that triggered this alert. In the other case the link will navigate to the rule's status page.", + "description": "Navigate to Discover and show the records that triggered + the alert when the rule is created in Discover. Otherwise, navigate to the status page for the rule.", "name": "link", }, ], @@ -82,10 +82,6 @@ describe('alertType', () => { "description": "A function to determine if the threshold was met.", "name": "thresholdComparator", }, - Object { - "description": "The type of search.", - "name": "searchType", - }, Object { "description": "Serialized search source fields used to fetch the documents from Elasticsearch.", "name": "searchConfiguration", From 63fb6b3f8cf9321758058b3c94431203c3423a2b Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 18 Mar 2022 11:59:09 +0500 Subject: [PATCH 56/67] [Discover] close popover on alert rule creation --- .../main/components/top_nav/open_alerts_popover.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 08536a06bf8ab..8dc751696774f 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -33,12 +33,10 @@ export function AlertsPopover(props: AlertsPopoverProps) { const searchSource = props.searchSource; const services = useDiscoverServices(); const { triggersActionsUi } = services; - const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); + const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); + + const onCloseAlertFlyout = useCallback(() => props.onClose(), [props]); - const onCloseAlertFlyout = useCallback( - () => setAlertFlyoutVisibility(false), - [setAlertFlyoutVisibility] - ); /** * Provides the default parameters used to initialize the new rule */ From 14afdc6f4017dd28d748c087a12445d29de562b1 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 21 Mar 2022 18:40:06 +0500 Subject: [PATCH 57/67] [Discover] apply suggestions --- .../data/public/ui/filter_bar/filter_item.tsx | 12 +- .../top_nav/open_alerts_popover.tsx | 21 +-- .../view_alert/view_alert_route.tsx | 155 +++++------------- .../view_alert/view_alert_utils.tsx | 116 +++++++++++++ .../es_query/expression/expression.tsx | 10 +- .../expression/read_only_filter_items.tsx | 6 +- .../expression/search_source_expression.tsx | 5 +- 7 files changed, 186 insertions(+), 139 deletions(-) create mode 100644 src/plugins/discover/public/application/view_alert/view_alert_utils.tsx diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 7f203965a9a16..5f57072425844 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CommonProps, EuiContextMenu, EuiPopover, EuiPopoverProps } from '@elastic/eui'; +import { EuiContextMenu, EuiPopover, EuiPopoverProps } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n-react'; import { Filter, @@ -40,6 +40,8 @@ export interface FilterItemProps { readonly?: boolean; } +type FilterPopoverProps = HTMLAttributes & EuiPopoverProps; + interface LabelOptions { title: string; status: FilterLabelStatus; @@ -355,14 +357,14 @@ export function FilterItem(props: FilterItemProps) { valueLabel: valueLabelConfig.title, filterLabelStatus: valueLabelConfig.status, errorMessage: valueLabelConfig.message, - className: getClasses(filter.meta.negate ?? false, valueLabelConfig), - iconOnClick: () => props.onRemove(), + className: getClasses(!!filter.meta.negate, valueLabelConfig), + iconOnClick: props.onRemove, onClick: handleBadgeClick, - ['data-test-subj']: getDataTestSubj(valueLabelConfig), + 'data-test-subj': getDataTestSubj(valueLabelConfig), readonly: props.readonly, }; - const popoverProps: CommonProps & HTMLAttributes & EuiPopoverProps = { + const popoverProps: FilterPopoverProps = { id: `popoverFor_filter${id}`, className: `globalFilterItem__popover`, anchorClassName: `globalFilterItem__popoverAnchor`, diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 8dc751696774f..2a9279ab22cbf 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -28,15 +28,12 @@ interface AlertsPopoverProps { searchSource: ISearchSource; } -export function AlertsPopover(props: AlertsPopoverProps) { - const dataView = props.searchSource.getField('index')!; - const searchSource = props.searchSource; +export function AlertsPopover({ searchSource, anchorElement, onClose }: AlertsPopoverProps) { + const dataView = searchSource.getField('index')!; const services = useDiscoverServices(); const { triggersActionsUi } = services; const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); - const onCloseAlertFlyout = useCallback(() => props.onClose(), [props]); - /** * Provides the default parameters used to initialize the new rule */ @@ -61,14 +58,14 @@ export function AlertsPopover(props: AlertsPopoverProps) { } return triggersActionsUi?.getAddAlertFlyout({ consumer: 'discover', - onClose: onCloseAlertFlyout, + onClose, canChangeTrigger: false, ruleTypeId: ALERT_TYPE_ID, initialValues: { params: getParams(), }, }); - }, [getParams, onCloseAlertFlyout, triggersActionsUi, alertFlyoutVisible]); + }, [getParams, onClose, triggersActionsUi, alertFlyoutVisible]); const hasTimeFieldName = dataView.timeFieldName; let createSearchThresholdRuleLink = ( @@ -134,8 +131,8 @@ export function AlertsPopover(props: AlertsPopoverProps) { {SearchThresholdAlertFlyout} @@ -144,7 +141,7 @@ export function AlertsPopover(props: AlertsPopoverProps) { ); } -function onClose() { +function closeAlertsPopover() { ReactDOM.unmountComponentAtNode(container); document.body.removeChild(container); isOpen = false; @@ -162,7 +159,7 @@ export function openAlertsPopover({ services: DiscoverServices; }) { if (isOpen) { - onClose(); + closeAlertsPopover(); return; } @@ -173,7 +170,7 @@ export function openAlertsPopover({ diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 56556384a8a6f..82481660d339c 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -5,71 +5,19 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useEffect, useMemo } from 'react'; + +import { useEffect, useMemo } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; import { sha256 } from 'js-sha256'; -import { i18n } from '@kbn/i18n'; -import { ToastsStart } from 'kibana/public'; import type { Alert } from '../../../../../../x-pack/plugins/alerting/common'; -import type { AlertTypeParams } from '../../../../../../x-pack/plugins/alerting/common'; -import { getTime, SerializedSearchSourceFields } from '../../../../data/common'; -import type { Filter, TimeRange } from '../../../../data/public'; -import { MarkdownSimple, toMountPoint } from '../../../../kibana_react/public'; +import { getTime, IndexPattern } from '../../../../data/common'; +import type { Filter } from '../../../../data/public'; import { DiscoverAppLocatorParams } from '../../locator'; import { useDiscoverServices } from '../../utils/use_discover_services'; - -interface SearchThresholdAlertParams extends AlertTypeParams { - searchConfiguration: SerializedSearchSourceFields; -} - -interface QueryParams { - from: string | null; - to: string | null; - checksum: string | null; -} +import { getAlertUtils, QueryParams, SearchThresholdAlertParams } from './view_alert_utils'; type NonNullableEntry = { [K in keyof T]: NonNullable }; -const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; -const DISCOVER_MAIN_ROUTE = '/'; - -const displayError = (title: string, errorMessage: string, toastNotifications: ToastsStart) => { - toastNotifications.addDanger({ - title, - text: toMountPoint({errorMessage}), - }); -}; - -const displayRuleChangedWarn = (toastNotifications: ToastsStart) => { - const warnTitle = i18n.translate('discover.viewAlert.alertRuleChangedWarnTitle', { - defaultMessage: 'Alert rule has changed', - }); - const warnDescription = i18n.translate('discover.viewAlert.alertRuleChangedWarnDescription', { - defaultMessage: `The displayed documents might not match the documents that triggered the alert - because the rule configuration changed.`, - }); - - toastNotifications.addWarning({ - title: warnTitle, - text: toMountPoint({warnDescription}), - }); -}; - -const displayPossibleDocsDiffInfoAlert = (toastNotifications: ToastsStart) => { - const infoTitle = i18n.translate('discover.viewAlert.documentsMayVaryInfoTitle', { - defaultMessage: 'Displayed documents may vary', - }); - const infoDescription = i18n.translate('discover.viewAlert.documentsMayVaryInfoDescription', { - defaultMessage: `The displayed documents might differ from the documents that triggered the alert. - Some documents might have been added or deleted.`, - }); - - toastNotifications.addInfo({ - title: infoTitle, - text: toMountPoint({infoDescription}), - }); -}; - const getCurrentChecksum = (params: SearchThresholdAlertParams) => sha256.create().update(JSON.stringify(params)).hex(); @@ -77,6 +25,23 @@ const isActualAlert = (queryParams: QueryParams): queryParams is NonNullableEntr return Boolean(queryParams.from && queryParams.to && queryParams.checksum); }; +const buildTimeRangeFilter = ( + dataView: IndexPattern, + fetchedAlert: Alert, + timeFieldName: string +) => { + const filter = getTime(dataView, { + from: `now-${fetchedAlert.params.timeWindowSize}${fetchedAlert.params.timeWindowUnit}`, + to: 'now', + }); + return { + from: filter?.query.range[timeFieldName].gte, + to: filter?.query.range[timeFieldName].lte, + }; +}; + +const DISCOVER_MAIN_ROUTE = '/'; + export function ViewAlertRoute() { const { core, data, locator, toastNotifications } = useDiscoverServices(); const { id } = useParams<{ id: string }>(); @@ -94,56 +59,29 @@ export function ViewAlertRoute() { [query] ); - const openConcreteAlert = isActualAlert(queryParams); + const openActualAlert = useMemo(() => isActualAlert(queryParams), [queryParams]); useEffect(() => { - const fetchAlert = async () => { - try { - return await core.http.get>( - `${LEGACY_BASE_ALERT_API_PATH}/alert/${id}` - ); - } catch (error) { - const errorTitle = i18n.translate('discover.viewAlert.alertRuleFetchErrorTitle', { - defaultMessage: 'Error fetching alert rule', - }); - displayError(errorTitle, error.message, toastNotifications); - } - }; - - const fetchSearchSource = async (fetchedAlert: Alert) => { - try { - return await data.search.searchSource.create(fetchedAlert.params.searchConfiguration); - } catch (error) { - const errorTitle = i18n.translate('discover.viewAlert.searchSourceErrorTitle', { - defaultMessage: 'Error fetching search source', - }); - displayError(errorTitle, error.message, toastNotifications); - } - }; - - const showDataViewFetchError = (alertId: string) => { - const errorTitle = i18n.translate('discover.viewAlert.dataViewErrorTitle', { - defaultMessage: 'Error fetching data view', - }); - displayError( - errorTitle, - new Error(`Data view failure of the alert rule with id ${alertId}.`).message, - toastNotifications - ); - }; + const { + fetchAlert, + fetchSearchSource, + displayRuleChangedWarn, + displayPossibleDocsDiffInfoAlert, + showDataViewFetchError, + } = getAlertUtils(toastNotifications, core, data); const navigateToResults = async () => { - const fetchedAlert = await fetchAlert(); + const fetchedAlert = await fetchAlert(id); if (!fetchedAlert) { history.push(DISCOVER_MAIN_ROUTE); return; } const calculatedChecksum = getCurrentChecksum(fetchedAlert.params); - if (openConcreteAlert && calculatedChecksum !== queryParams.checksum) { - displayRuleChangedWarn(toastNotifications); - } else if (openConcreteAlert && calculatedChecksum === queryParams.checksum) { - displayPossibleDocsDiffInfoAlert(toastNotifications); + if (openActualAlert && calculatedChecksum !== queryParams.checksum) { + displayRuleChangedWarn(); + } else if (openActualAlert && calculatedChecksum === queryParams.checksum) { + displayPossibleDocsDiffInfoAlert(); } const fetchedSearchSource = await fetchSearchSource(fetchedAlert); @@ -160,29 +98,20 @@ export function ViewAlertRoute() { return; } - let timeRange: TimeRange; - if (openConcreteAlert) { - timeRange = { from: queryParams.from, to: queryParams.to }; - } else { - const filter = getTime(dataView, { - from: `now-${fetchedAlert.params.timeWindowSize}${fetchedAlert.params.timeWindowUnit}`, - to: 'now', - }); - timeRange = { - from: filter?.query.range[timeFieldName].gte, - to: filter?.query.range[timeFieldName].lte, - }; - } - + const timeRange = openActualAlert + ? { from: queryParams.from, to: queryParams.to } + : buildTimeRangeFilter(dataView, fetchedAlert, timeFieldName); const state: DiscoverAppLocatorParams = { query: fetchedSearchSource.getField('query') || data.query.queryString.getDefaultQuery(), indexPatternId: dataView.id, timeRange, }; + const filters = fetchedSearchSource.getField('filter'); if (filters) { state.filters = filters as Filter[]; } + await locator.navigate(state); }; @@ -196,7 +125,9 @@ export function ViewAlertRoute() { id, queryParams, history, - openConcreteAlert, + openActualAlert, + core, + data, ]); return null; diff --git a/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx new file mode 100644 index 0000000000000..b61f0c9a8720c --- /dev/null +++ b/src/plugins/discover/public/application/view_alert/view_alert_utils.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { CoreStart, ToastsStart } from 'kibana/public'; +import type { Alert } from '../../../../../../x-pack/plugins/alerting/common'; +import type { AlertTypeParams } from '../../../../../../x-pack/plugins/alerting/common'; +import { SerializedSearchSourceFields } from '../../../../data/common'; +import type { DataPublicPluginStart } from '../../../../data/public'; +import { MarkdownSimple, toMountPoint } from '../../../../kibana_react/public'; + +export interface SearchThresholdAlertParams extends AlertTypeParams { + searchConfiguration: SerializedSearchSourceFields; +} + +export interface QueryParams { + from: string | null; + to: string | null; + checksum: string | null; +} + +const LEGACY_BASE_ALERT_API_PATH = '/api/alerts'; + +export const getAlertUtils = ( + toastNotifications: ToastsStart, + core: CoreStart, + data: DataPublicPluginStart +) => { + const showDataViewFetchError = (alertId: string) => { + const errorTitle = i18n.translate('discover.viewAlert.dataViewErrorTitle', { + defaultMessage: 'Error fetching data view', + }); + toastNotifications.addDanger({ + title: errorTitle, + text: toMountPoint( + + {new Error(`Data view failure of the alert rule with id ${alertId}.`).message} + + ), + }); + }; + + const displayRuleChangedWarn = () => { + const warnTitle = i18n.translate('discover.viewAlert.alertRuleChangedWarnTitle', { + defaultMessage: 'Alert rule has changed', + }); + const warnDescription = i18n.translate('discover.viewAlert.alertRuleChangedWarnDescription', { + defaultMessage: `The displayed documents might not match the documents that triggered the alert + because the rule configuration changed.`, + }); + + toastNotifications.addWarning({ + title: warnTitle, + text: toMountPoint({warnDescription}), + }); + }; + + const displayPossibleDocsDiffInfoAlert = () => { + const infoTitle = i18n.translate('discover.viewAlert.documentsMayVaryInfoTitle', { + defaultMessage: 'Displayed documents may vary', + }); + const infoDescription = i18n.translate('discover.viewAlert.documentsMayVaryInfoDescription', { + defaultMessage: `The displayed documents might differ from the documents that triggered the alert. + Some documents might have been added or deleted.`, + }); + + toastNotifications.addInfo({ + title: infoTitle, + text: toMountPoint({infoDescription}), + }); + }; + + const fetchAlert = async (id: string) => { + try { + return await core.http.get>( + `${LEGACY_BASE_ALERT_API_PATH}/alert/${id}` + ); + } catch (error) { + const errorTitle = i18n.translate('discover.viewAlert.alertRuleFetchErrorTitle', { + defaultMessage: 'Error fetching alert rule', + }); + toastNotifications.addDanger({ + title: errorTitle, + text: toMountPoint({error.message}), + }); + } + }; + + const fetchSearchSource = async (fetchedAlert: Alert) => { + try { + return await data.search.searchSource.create(fetchedAlert.params.searchConfiguration); + } catch (error) { + const errorTitle = i18n.translate('discover.viewAlert.searchSourceErrorTitle', { + defaultMessage: 'Error fetching search source', + }); + toastNotifications.addDanger({ + title: errorTitle, + text: toMountPoint({error.message}), + }); + } + }; + + return { + displayRuleChangedWarn, + displayPossibleDocsDiffInfoAlert, + showDataViewFetchError, + fetchAlert, + fetchSearchSource, + }; +}; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx index 2195cc643122f..2c825cdc3c286 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/expression.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import 'brace/theme/github'; @@ -51,13 +51,13 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< ); return ( - + <> {hasExpressionErrors && ( - + <> - + )} {isSearchSource ? ( @@ -65,6 +65,6 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< ) : ( )} - + ); }; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx index f62d3b9871eb3..b4b0c6ae9ac5d 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/read_only_filter_items.tsx @@ -23,6 +23,8 @@ interface ReadOnlyFilterItemsProps { indexPatterns: IIndexPattern[]; } +const noOp = () => {}; + export const ReadOnlyFilterItems = ({ filters, indexPatterns }: ReadOnlyFilterItemsProps) => { const { uiSettings } = useKibana().services; @@ -34,8 +36,8 @@ export const ReadOnlyFilterItems = ({ filters, indexPatterns }: ReadOnlyFilterIt key={`${filter.meta.key}${filterValue}`} id={`${index}`} filter={filter} - onUpdate={() => {}} - onRemove={() => {}} + onUpdate={noOp} + onRemove={noOp} indexPatterns={indexPatterns} uiSettings={uiSettings!} readonly diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx index 1d0406090e382..2e99fa8e1356b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.tsx @@ -106,7 +106,7 @@ export const SearchSourceExpression = ({ const filters = (usedSearchSource.getField('filter') as Filter[]).filter( ({ meta }) => !meta.disabled ); - const indexPatterns = [dataView]; + const dataViews = [dataView]; return ( @@ -148,7 +148,7 @@ export const SearchSourceExpression = ({ className="dscExpressionParam searchSourceAlertFilters" title={'sas'} description={'Filter'} - value={} + value={} display="columns" /> )} @@ -214,7 +214,6 @@ export const SearchSourceExpression = ({ setParam('size', updatedValue); }} /> - ); From 877748ab73f201e2babb265c75581f82bbbbc8db Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 23 Mar 2022 17:44:04 +0500 Subject: [PATCH 58/67] [Discover] add first functional test --- .../top_nav/open_alerts_popover.tsx | 6 +- .../apps/discover/_search_source_alert.ts | 254 ++++++++++++++++++ test/functional/apps/discover/index.ts | 1 + 3 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 test/functional/apps/discover/_search_source_alert.ts diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 2a9279ab22cbf..21d560ccb539d 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -69,7 +69,11 @@ export function AlertsPopover({ searchSource, anchorElement, onClose }: AlertsPo const hasTimeFieldName = dataView.timeFieldName; let createSearchThresholdRuleLink = ( - setAlertFlyoutVisibility(true)} disabled={!hasTimeFieldName}> + setAlertFlyoutVisibility(true)} + disabled={!hasTimeFieldName} + > + es.index({ + index: SOURCE_DATA_INDEX, + body: { + settings: { number_of_shards: 1 }, + mappings: { + properties: { + '@timestamp': { type: 'date' }, + message: { type: 'text' }, + }, + }, + }, + }); + + const generateNewDocs = async (docsNumber: number) => { + const mockMessages = new Array(docsNumber).map((current) => `msg-${current}`); + const dateNow = new Date().toISOString(); + for (const message of mockMessages) { + await es.transport.request({ + path: `/${SOURCE_DATA_INDEX}/_doc`, + method: 'POST', + body: { + '@timestamp': dateNow, + message, + }, + }); + } + }; + + const createOutputDataIndex = () => + es.index({ + index: OUTPUT_DATA_INDEX, + body: { + settings: { + number_of_shards: 1, + }, + mappings: { + properties: { + rule_id: { type: 'text' }, + rule_name: { type: 'text' }, + alert_id: { type: 'text' }, + context_message: { type: 'text' }, + }, + }, + }, + }); + + const deleteIndexes = () => + asyncForEach([SOURCE_DATA_INDEX, OUTPUT_DATA_INDEX], async (indexName) => { + await es.transport.request({ + path: `/${indexName}`, + method: 'DELETE', + }); + }); + + const deleteAlerts = (alertIds: string[]) => + asyncForEach(alertIds, async (alertId: string) => { + await supertest + .delete(`/api/alerting/rule/${alertId}`) + .set('kbn-xsrf', 'foo') + .expect(204, ''); + }); + + const getAlertsByName = async (name: string) => { + const { + body: { data: alerts }, + } = await supertest + .get(`/api/alerting/rules/_find?search=${name}&search_fields=name`) + .expect(200); + + return alerts; + }; + + const createDataViews = () => + asyncMap( + [SOURCE_DATA_INDEX, OUTPUT_DATA_INDEX], + async (dataView: string) => + await supertest + .post(`/api/data_views/data_view`) + .set('kbn-xsrf', 'foo') + .send({ data_view: { title: dataView, timeFieldName: '@timestamp' } }) + .expect(200) + ); + + const createConnector = async (): Promise => { + const { body: createdAction } = await supertest + .post(`/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'search-source-alert-test-connector', + connector_type_id: ACTION_TYPE_ID, + config: { index: OUTPUT_DATA_INDEX }, + secrets: {}, + }) + .expect(200); + + return createdAction.id; + }; + + const deleteConnector = (connectorId: string) => + supertest + .delete(`/api/actions/connector/${connectorId}`) + .set('kbn-xsrf', 'foo') + .expect(204, ''); + + const deleteDataViews = (alertIds: string[]) => + asyncForEach( + alertIds, + async (dataView: string) => + await supertest + .delete(`/api/data_views/data_view/${dataView}`) + .set('kbn-xsrf', 'foo') + .expect(200) + ); + + const defineSearchSourceAlert = async (alertName: string) => { + await testSubjects.click('discoverAlertsButton'); + await testSubjects.click('discoverCreateAlertButton'); + + await testSubjects.setValue('ruleNameInput', alertName); + await testSubjects.click('thresholdPopover'); + await testSubjects.setValue('alertThresholdInput', '3'); + await testSubjects.click('.index-ActionTypeSelectOption'); + + await monacoEditor.setCodeEditorValue(`{ + "rule_id": "{{ruleId}}", + "rule_name": "{{ruleName}}", + "alert_id": "{{alertId}}", + "context_message": "{{context.message}}" + }`); + await testSubjects.click('saveRuleButton'); + }; + + const getLastToast = async () => { + const toastList = await testSubjects.find('globalToastList'); + const titleElement = await toastList.findByCssSelector('.euiToastHeader'); + const title: string = await titleElement.getVisibleText(); + const messageElement = await toastList.findByCssSelector('.euiToastBody'); + const message: string = await messageElement.getVisibleText(); + return { message, title }; + }; + + describe('Search source Alert', () => { + const ruleName = 'test-search-source-alert'; + let sourceDataViewId: string; + let outputDataViewId: string; + let connectorId: string; + + beforeEach(async () => { + // init test data + await createSourceIndex(); + await generateNewDocs(5); + await createOutputDataIndex(); + const [sourceDataViewResponse, outputDataViewResponse] = await createDataViews(); + connectorId = await createConnector(); + sourceDataViewId = sourceDataViewResponse.body.data_view.id; + outputDataViewId = outputDataViewResponse.body.data_view.id; + }); + + afterEach(async () => { + // clean up test data + await deleteIndexes(); + await deleteDataViews([sourceDataViewId, outputDataViewId]); + await deleteConnector(connectorId); + const alertsToDelete = await getAlertsByName(ruleName); + await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); + }); + + it('should successfully trigger alert', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.discover.selectIndexPattern(SOURCE_DATA_INDEX); + await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes'); + + // create an alert + await defineSearchSourceAlert(ruleName); + + // open output index + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX); + + const [{ id: alertId }] = await getAlertsByName(ruleName); + await queryBar.setQuery(`alert_id:${alertId}`); + await queryBar.submitQuery(); + + // waiting for alert to be triggered + await retry.waitFor('doc count to be 1', async () => { + const docCount = await dataGrid.getDocCount(); + return docCount >= 1; + }); + + // getting link + await dataGrid.clickRowToggle(); + await testSubjects.click('collapseBtn'); + const contextMessageElement = await testSubjects.find( + 'tableDocViewRow-context_message-value' + ); + const contextMessage = await contextMessageElement.getVisibleText(); + const [, link] = contextMessage.split(`Link\: `); + const baseUrl = deployment.getHostPort(); + + // following ling provided by alert to see documents triggered the alert + await browser.navigateTo(baseUrl + link); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const { message, title } = await getLastToast(); + const docsNumber = await dataGrid.getDocCount(); + + expect(await browser.getCurrentUrl()).to.contain(sourceDataViewId); + expect(docsNumber).to.be(5); + expect(title).to.be.equal('Displayed documents may vary'); + expect(message).to.be.equal( + 'The displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.' + ); + }); + }); +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index d2b627c175fcc..dbddc7a7fd3c3 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -56,5 +56,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_context_encoded_url_param')); loadTestFile(require.resolve('./_data_view_editor')); loadTestFile(require.resolve('./_empty_state')); + loadTestFile(require.resolve('./_search_source_alert')); }); } From c2a7b102b80d2711461cfe55c6ad9e9165f11d25 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 23 Mar 2022 22:15:57 +0500 Subject: [PATCH 59/67] [Discover] implement tests --- .../apps/discover/_search_source_alert.ts | 190 ++++++++++++------ 1 file changed, 130 insertions(+), 60 deletions(-) diff --git a/test/functional/apps/discover/_search_source_alert.ts b/test/functional/apps/discover/_search_source_alert.ts index 8dcd41f00822c..e30c9dd54e517 100644 --- a/test/functional/apps/discover/_search_source_alert.ts +++ b/test/functional/apps/discover/_search_source_alert.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { asyncMap, asyncForEach } from '@kbn/std'; +import { last } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -27,12 +28,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); const supertest = getService('supertest'); - const queryBar = getService('queryBar'); const SOURCE_DATA_INDEX = 'search-source-alert'; const OUTPUT_DATA_INDEX = 'search-source-alert-output'; const ACTION_TYPE_ID = '.index'; + const RULE_NAME = 'test-search-source-alert'; + let sourceDataViewId: string; + let outputDataViewId: string; + let connectorId: string; const createSourceIndex = () => es.index({ @@ -81,14 +85,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }, }); - const deleteIndexes = () => - asyncForEach([SOURCE_DATA_INDEX, OUTPUT_DATA_INDEX], async (indexName) => { - await es.transport.request({ - path: `/${indexName}`, - method: 'DELETE', - }); - }); - const deleteAlerts = (alertIds: string[]) => asyncForEach(alertIds, async (alertId: string) => { await supertest @@ -133,15 +129,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return createdAction.id; }; - const deleteConnector = (connectorId: string) => - supertest - .delete(`/api/actions/connector/${connectorId}`) - .set('kbn-xsrf', 'foo') - .expect(204, ''); + const deleteConnector = (id: string) => + supertest.delete(`/api/actions/connector/${id}`).set('kbn-xsrf', 'foo').expect(204, ''); - const deleteDataViews = (alertIds: string[]) => + const deleteDataViews = (dataViews: string[]) => asyncForEach( - alertIds, + dataViews, async (dataView: string) => await supertest .delete(`/api/data_views/data_view/${dataView}`) @@ -169,21 +162,69 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const getLastToast = async () => { const toastList = await testSubjects.find('globalToastList'); - const titleElement = await toastList.findByCssSelector('.euiToastHeader'); - const title: string = await titleElement.getVisibleText(); - const messageElement = await toastList.findByCssSelector('.euiToastBody'); - const message: string = await messageElement.getVisibleText(); + const titles = await toastList.findAllByCssSelector('.euiToastHeader'); + const lastTitleElement = last(titles)!; + const title = await lastTitleElement.getVisibleText(); + const messages = await toastList.findAllByCssSelector('.euiToastBody'); + const lastMessageElement = last(messages)!; + const message = await lastMessageElement.getVisibleText(); return { message, title }; }; - describe('Search source Alert', () => { - const ruleName = 'test-search-source-alert'; - let sourceDataViewId: string; - let outputDataViewId: string; - let connectorId: string; + const openOutputIndex = async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX); + + const [{ id: alertId }] = await getAlertsByName(RULE_NAME); + await queryBar.setQuery(`alert_id:${alertId}`); + await queryBar.submitQuery(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }; + + const getResultsLink = async () => { + // getting the link + await dataGrid.clickRowToggle(); + await testSubjects.click('collapseBtn'); + const contextMessageElement = await testSubjects.find('tableDocViewRow-context_message-value'); + const contextMessage = await contextMessageElement.getVisibleText(); + const [, link] = contextMessage.split(`Link\: `); + + return link; + }; - beforeEach(async () => { - // init test data + const navigateToDiscover = async (link: string) => { + // following ling provided by alert to see documents triggered the alert + const baseUrl = deployment.getHostPort(); + await browser.navigateTo(baseUrl + link); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await retry.waitFor('navigate to discover', async () => { + const currentUrl = await browser.getCurrentUrl(); + return currentUrl.includes(sourceDataViewId); + }); + }; + + const navigateToResults = async () => { + const link = await getResultsLink(); + await navigateToDiscover(link); + }; + + const openAlertRule = async () => { + await PageObjects.common.navigateToApp('management'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await testSubjects.click('triggersActions'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const rulesList = await testSubjects.find('rulesList'); + const alertRule = await rulesList.findByCssSelector('[title="test-search-source-alert"]'); + await alertRule.click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }; + + describe('Search source Alert', () => { + before(async () => { await createSourceIndex(); await generateNewDocs(5); await createOutputDataIndex(); @@ -193,62 +234,91 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { outputDataViewId = outputDataViewResponse.body.data_view.id; }); - afterEach(async () => { - // clean up test data - await deleteIndexes(); + after(async () => { + // delete only remaining output index + await es.transport.request({ + path: `/${OUTPUT_DATA_INDEX}`, + method: 'DELETE', + }); await deleteDataViews([sourceDataViewId, outputDataViewId]); await deleteConnector(connectorId); - const alertsToDelete = await getAlertsByName(ruleName); + const alertsToDelete = await getAlertsByName(RULE_NAME); await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); }); - it('should successfully trigger alert', async () => { + it('should navigate to discover via view in app link', async () => { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.selectIndexPattern(SOURCE_DATA_INDEX); await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes'); // create an alert - await defineSearchSourceAlert(ruleName); + await defineSearchSourceAlert(RULE_NAME); + await PageObjects.header.waitUntilLoadingHasFinished(); - // open output index - await PageObjects.common.navigateToApp('discover'); - await PageObjects.discover.waitUntilSearchingHasFinished(); - await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX); + await openAlertRule(); - const [{ id: alertId }] = await getAlertsByName(ruleName); - await queryBar.setQuery(`alert_id:${alertId}`); - await queryBar.submitQuery(); + await testSubjects.click('ruleDetails-viewInApp'); + await PageObjects.header.waitUntilLoadingHasFinished(); - // waiting for alert to be triggered - await retry.waitFor('doc count to be 1', async () => { - const docCount = await dataGrid.getDocCount(); - return docCount >= 1; + await retry.waitFor('navigate to discover', async () => { + const currentUrl = await browser.getCurrentUrl(); + return currentUrl.includes(sourceDataViewId); }); - // getting link - await dataGrid.clickRowToggle(); - await testSubjects.click('collapseBtn'); - const contextMessageElement = await testSubjects.find( - 'tableDocViewRow-context_message-value' - ); - const contextMessage = await contextMessageElement.getVisibleText(); - const [, link] = contextMessage.split(`Link\: `); - const baseUrl = deployment.getHostPort(); + expect(await dataGrid.getDocCount()).to.be(5); + }); - // following ling provided by alert to see documents triggered the alert - await browser.navigateTo(baseUrl + link); - await PageObjects.discover.waitUntilSearchingHasFinished(); + it('should open documents triggered the alert', async () => { + await openOutputIndex(); + await navigateToResults(); const { message, title } = await getLastToast(); - const docsNumber = await dataGrid.getDocCount(); - - expect(await browser.getCurrentUrl()).to.contain(sourceDataViewId); - expect(docsNumber).to.be(5); + expect(await dataGrid.getDocCount()).to.be(5); expect(title).to.be.equal('Displayed documents may vary'); expect(message).to.be.equal( 'The displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.' ); }); + + it('should display warning about updated alert rule', async () => { + await openAlertRule(); + + // change rule configuration + await testSubjects.click('openEditRuleFlyoutButton'); + await testSubjects.click('thresholdPopover'); + await testSubjects.setValue('alertThresholdInput', '1'); + await testSubjects.click('saveEditedRuleButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await openOutputIndex(); + await navigateToResults(); + + const { message, title } = await getLastToast(); + expect(await dataGrid.getDocCount()).to.be(5); + expect(title).to.be.equal('Alert rule has changed'); + expect(message).to.be.equal( + 'The displayed documents might not match the documents that triggered the alert because the rule configuration changed.' + ); + }); + + it('should display not found index error', async () => { + await openOutputIndex(); + const link = await getResultsLink(); + await navigateToDiscover(link); + + await es.transport.request({ + path: `/${SOURCE_DATA_INDEX}`, + method: 'DELETE', + }); + await browser.refresh(); + + await navigateToDiscover(link); + + const { title } = await getLastToast(); + expect(title).to.be.equal( + 'No matching indices found: No indices match "search-source-alert"' + ); + }); }); } From a06d37b773bf37c37a13086c8e7672a9f4f3f55f Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 24 Mar 2022 10:19:37 +0100 Subject: [PATCH 60/67] Move functionals x-pack since ssl is needed --- test/functional/apps/discover/index.ts | 1 - .../functional_with_es_ssl/apps/discover/index.ts | 14 ++++++++++++++ .../apps/discover/search_source_alert.ts | 5 ++--- x-pack/test/functional_with_es_ssl/config.ts | 1 + 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/functional_with_es_ssl/apps/discover/index.ts rename test/functional/apps/discover/_search_source_alert.ts => x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts (98%) diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index dbddc7a7fd3c3..d2b627c175fcc 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -56,6 +56,5 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_context_encoded_url_param')); loadTestFile(require.resolve('./_data_view_editor')); loadTestFile(require.resolve('./_empty_state')); - loadTestFile(require.resolve('./_search_source_alert')); }); } diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/index.ts b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts new file mode 100644 index 0000000000000..93fe8e7da3f92 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ loadTestFile, getService }: FtrProviderContext) => { + describe('Discover alerting', function () { + this.tags('ciGroup10'); + loadTestFile(require.resolve('./search_source_alert')); + }); +}; diff --git a/test/functional/apps/discover/_search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts similarity index 98% rename from test/functional/apps/discover/_search_source_alert.ts rename to x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index e30c9dd54e517..874a39915b5be 100644 --- a/test/functional/apps/discover/_search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -1,9 +1,8 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import expect from '@kbn/expect'; diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index e537603a0113b..48095e905226d 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -48,6 +48,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { resolve(__dirname, './apps/triggers_actions_ui'), resolve(__dirname, './apps/uptime'), resolve(__dirname, './apps/ml'), + resolve(__dirname, './apps/discover'), ], apps: { ...xpackFunctionalConfig.get('apps'), From 5adae743e0d3108784fb4a5b0390ee01a5e19827 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 24 Mar 2022 12:19:39 +0100 Subject: [PATCH 61/67] Fix potential flakiness in functional test --- .../apps/discover/search_source_alert.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index 874a39915b5be..1fe6102b972a0 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -177,8 +177,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const [{ id: alertId }] = await getAlertsByName(RULE_NAME); await queryBar.setQuery(`alert_id:${alertId}`); - await queryBar.submitQuery(); - await PageObjects.discover.waitUntilSearchingHasFinished(); + await retry.waitForWithTimeout('doc table contains alert', 5000, async () => { + await queryBar.submitQuery(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + return (await dataGrid.getDocCount()) > 0; + }); }; const getResultsLink = async () => { From e4892eadb6c52c6f976b116d6b28731890398453 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 25 Mar 2022 15:02:15 +0500 Subject: [PATCH 62/67] [Discover] remove timeout waiter --- .../functional_with_es_ssl/apps/discover/search_source_alert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index 1fe6102b972a0..948e7e6757a74 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -177,7 +177,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const [{ id: alertId }] = await getAlertsByName(RULE_NAME); await queryBar.setQuery(`alert_id:${alertId}`); - await retry.waitForWithTimeout('doc table contains alert', 5000, async () => { + await retry.waitFor('document explorer contains alert', async () => { await queryBar.submitQuery(); await PageObjects.discover.waitUntilSearchingHasFinished(); return (await dataGrid.getDocCount()) > 0; From 622703cc5675dd97fa005e8503868f011264121d Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Sat, 26 Mar 2022 01:39:12 +0100 Subject: [PATCH 63/67] Fix functional test - adding permissions to fix the functional --- .../apps/discover/index.ts | 2 +- .../apps/discover/search_source_alert.ts | 3 +++ x-pack/test/functional_with_es_ssl/config.ts | 26 ++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/index.ts b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts index 93fe8e7da3f92..708da2f02da74 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile, getService }: FtrProviderContext) => { describe('Discover alerting', function () { - this.tags('ciGroup10'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./search_source_alert')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index 948e7e6757a74..b109c16498dc9 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const supertest = getService('supertest'); const queryBar = getService('queryBar'); + const security = getService('security'); const SOURCE_DATA_INDEX = 'search-source-alert'; const OUTPUT_DATA_INDEX = 'search-source-alert-output'; @@ -227,6 +228,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Search source Alert', () => { before(async () => { + await security.testUser.setRoles(['discover_alert']); await createSourceIndex(); await generateNewDocs(5); await createOutputDataIndex(); @@ -246,6 +248,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await deleteConnector(connectorId); const alertsToDelete = await getAlertsByName(RULE_NAME); await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); + await security.testUser.restoreDefaults(); }); it('should navigate to discover via view in app link', async () => { diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index bdeda74c44764..7b99aa0d7a895 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -45,11 +45,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { pageObjects, // list paths to the files that contain your plugins tests testFiles: [ + resolve(__dirname, './apps/discover'), resolve(__dirname, './apps/triggers_actions_ui'), resolve(__dirname, './apps/uptime'), resolve(__dirname, './apps/ml'), resolve(__dirname, './apps/cases'), - resolve(__dirname, './apps/discover'), ], apps: { ...xpackFunctionalConfig.get('apps'), @@ -108,6 +108,30 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, ], }, + discover_alert: { + kibana: [ + { + feature: { + actions: ['all'], + stackAlerts: ['all'], + discover: ['all'], + advancedSettings: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: [], + indices: [ + { + names: ['search-source-alert', 'search-source-alert-output'], + privileges: ['read', 'view_index_metadata', 'manage', 'create_index', 'index'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + }, }, defaultRoles: ['superuser'], }, From b48e03b025459d7c03c502ffadbbf1d3970ed7f8 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 28 Mar 2022 11:26:40 +0500 Subject: [PATCH 64/67] [Discover] add logger --- .../apps/discover/search_source_alert.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index b109c16498dc9..29732a38cc562 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -11,6 +11,7 @@ import { last } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); const es = getService('es'); const monacoEditor = getService('monacoEditor'); const PageObjects = getPageObjects([ @@ -232,8 +233,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await createSourceIndex(); await generateNewDocs(5); await createOutputDataIndex(); + + log.debug('create data views'); const [sourceDataViewResponse, outputDataViewResponse] = await createDataViews(); + + log.debug('create connector'); connectorId = await createConnector(); + sourceDataViewId = sourceDataViewResponse.body.data_view.id; outputDataViewId = outputDataViewResponse.body.data_view.id; }); From 2d7360cac41747679a37bc1fffde5afdb367cb14 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 28 Mar 2022 12:52:22 +0500 Subject: [PATCH 65/67] [Discover] add more log points --- .../apps/discover/search_source_alert.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index 29732a38cc562..fa20d2cac258c 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -230,8 +230,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Search source Alert', () => { before(async () => { await security.testUser.setRoles(['discover_alert']); + + log.debug('create source index'); await createSourceIndex(); + + log.debug('generate documents'); await generateNewDocs(5); + + log.debug('create output index'); await createOutputDataIndex(); log.debug('create data views'); From 048fefb18f9cfb9075e08329a6ac4666b053a5e0 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 28 Mar 2022 18:47:20 +0500 Subject: [PATCH 66/67] [Discover] wait for indices creation finished --- .../functional_with_es_ssl/apps/discover/search_source_alert.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index fa20d2cac258c..a60d4fa082469 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -240,6 +240,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('create output index'); await createOutputDataIndex(); + // wait for indices creation finished + await PageObjects.common.sleep(8000); log.debug('create data views'); const [sourceDataViewResponse, outputDataViewResponse] = await createDataViews(); From b6b273a3bef2e8414699fafd7825abf1a572c1b3 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 28 Mar 2022 21:05:26 +0200 Subject: [PATCH 67/67] Try to fix the functional flakiness - by creating data views in a serial way - lets see if that work --- .../apps/discover/search_source_alert.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts index a60d4fa082469..bae045fc93838 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover/search_source_alert.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { asyncMap, asyncForEach } from '@kbn/std'; +import { asyncForEach } from '@kbn/std'; import { last } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -104,16 +104,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return alerts; }; - const createDataViews = () => - asyncMap( - [SOURCE_DATA_INDEX, OUTPUT_DATA_INDEX], - async (dataView: string) => - await supertest - .post(`/api/data_views/data_view`) - .set('kbn-xsrf', 'foo') - .send({ data_view: { title: dataView, timeFieldName: '@timestamp' } }) - .expect(200) - ); + const createDataView = async (dataView: string) => { + log.debug(`create data view ${dataView}`); + return await supertest + .post(`/api/data_views/data_view`) + .set('kbn-xsrf', 'foo') + .send({ data_view: { title: dataView, timeFieldName: '@timestamp' } }) + .expect(200); + }; const createConnector = async (): Promise => { const { body: createdAction } = await supertest @@ -240,10 +238,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { log.debug('create output index'); await createOutputDataIndex(); - // wait for indices creation finished - await PageObjects.common.sleep(8000); log.debug('create data views'); - const [sourceDataViewResponse, outputDataViewResponse] = await createDataViews(); + const sourceDataViewResponse = await createDataView(SOURCE_DATA_INDEX); + const outputDataViewResponse = await createDataView(OUTPUT_DATA_INDEX); log.debug('create connector'); connectorId = await createConnector();