From c1a8b90f5125ca57b1565b567f8e1526e776ab7c Mon Sep 17 00:00:00 2001 From: Faisal Kanout Date: Tue, 28 Mar 2023 18:15:47 +0200 Subject: [PATCH] [AO] - Add scaffolding and the main chart to the Logs threshold Alert Details page (#153081) ## Summary ### This is a kickoff PR; more PRs will follow up. It closes #152738 Screenshot 2023-03-23 at 13 10 09 ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../alerting/logs/log_threshold/types.ts | 5 ++ .../http_api/log_alerts/chart_preview_data.ts | 7 +++ .../alert_details_app_section/index.tsx | 57 +++++++++++++++++++ .../alert_details_app_section/types.ts | 15 +++++ .../criterion_preview_chart.tsx | 7 +++ .../hooks/use_chart_preview_data.tsx | 22 +++++-- .../log_threshold/log_threshold_rule_type.tsx | 2 + x-pack/plugins/infra/public/types.ts | 5 ++ .../log_threshold_chart_preview.ts | 11 ++-- .../log_threshold_executor.test.ts | 16 +++--- .../log_threshold/log_threshold_executor.ts | 22 ++++--- .../routes/log_alerts/chart_preview_data.ts | 5 +- 12 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx create mode 100644 x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index c0ad0af7e69fe..2eec5b035c793 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -401,3 +401,8 @@ export const isOptimizableGroupedThreshold = ( return false; } }; + +export interface ExecutionTimeRange { + gte?: number; + lte: number; +} diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 15f0df5222e7d..afdecfc64b906 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -85,6 +85,13 @@ export const getLogAlertsChartPreviewDataRequestPayloadRT = rt.type({ logView: persistedLogViewReferenceRT, alertParams: getLogAlertsChartPreviewDataAlertParamsSubsetRT, buckets: rt.number, + executionTimeRange: rt.union([ + rt.undefined, + rt.type({ + gte: rt.number, + lte: rt.number, + }), + ]), }), }); diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx new file mode 100644 index 0000000000000..ad5ef8a99f23b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx @@ -0,0 +1,57 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils'; +import moment from 'moment'; +import React from 'react'; +import { type PartialCriterion } from '../../../../../common/alerting/logs/log_threshold'; +import { CriterionPreview } from '../expression_editor/criterion_preview_chart'; +import { AlertDetailsAppSectionProps } from './types'; + +const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) => { + const ruleWindowSizeMS = moment + .duration(rule.params.timeSize, rule.params.timeUnit) + .asMilliseconds(); + const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000; + const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS; + /** + * This is part or the requirements (RFC). + * If the alert is less than 20 units of `FOR THE LAST ` then we should draw a time range of 20 units. + * IE. The user set "FOR THE LAST 5 minutes" at a minimum we should show 100 minutes. + */ + const rangeFrom = + alertDurationMS < TWENTY_TIMES_RULE_WINDOW_MS + ? Number(moment(alert.start).subtract(TWENTY_TIMES_RULE_WINDOW_MS, 'millisecond').format('x')) + : Number(moment(alert.start).subtract(ruleWindowSizeMS, 'millisecond').format('x')); + + const rangeTo = alert.active + ? Date.now() + : Number(moment(alert.fields[ALERT_END]).add(ruleWindowSizeMS, 'millisecond').format('x')); + + return ( + // Create a chart per-criteria + + {rule.params.criteria.map((criteria) => { + const chartCriterion = criteria as PartialCriterion; + return ( + + + + ); + })} + + ); +}; +// eslint-disable-next-line import/no-default-export +export default AlertDetailsAppSection; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts new file mode 100644 index 0000000000000..8a4b23b630b7b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/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. + */ + +import { Rule } from '@kbn/alerting-plugin/common'; +import { TopAlert } from '@kbn/observability-plugin/public'; +import { PartialRuleParams } from '../../../../../common/alerting/logs/log_threshold'; + +export interface AlertDetailsAppSectionProps { + rule: Rule; + alert: TopAlert; +} diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 5ffefdd57119e..e5ea692ce7792 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -21,6 +21,7 @@ import { import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ExecutionTimeRange } from '../../../../types'; import { ChartContainer, LoadingState, @@ -56,6 +57,7 @@ interface Props { chartCriterion: Partial; sourceId: string; showThreshold: boolean; + executionTimeRange?: ExecutionTimeRange; } export const CriterionPreview: React.FC = ({ @@ -63,6 +65,7 @@ export const CriterionPreview: React.FC = ({ chartCriterion, sourceId, showThreshold, + executionTimeRange, }) => { const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => { const { field, comparator, value } = chartCriterion; @@ -106,6 +109,7 @@ export const CriterionPreview: React.FC = ({ threshold={ruleParams.count} chartAlertParams={chartAlertParams} showThreshold={showThreshold} + executionTimeRange={executionTimeRange} /> ); }; @@ -116,6 +120,7 @@ interface ChartProps { threshold?: Threshold; chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset; showThreshold: boolean; + executionTimeRange?: ExecutionTimeRange; } const CriterionPreviewChart: React.FC = ({ @@ -124,6 +129,7 @@ const CriterionPreviewChart: React.FC = ({ threshold, chartAlertParams, showThreshold, + executionTimeRange, }) => { const { uiSettings } = useKibana().services; const isDarkMode = uiSettings?.get('theme:darkMode') || false; @@ -138,6 +144,7 @@ const CriterionPreviewChart: React.FC = ({ sourceId, ruleParams: chartAlertParams, buckets, + executionTimeRange, }); useDebounce( diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx index 0b99cea2fd7c9..913962f8703d1 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx @@ -8,6 +8,7 @@ import { useState, useMemo } from 'react'; import { HttpHandler } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ExecutionTimeRange } from '../../../../../types'; import { useTrackedPromise } from '../../../../../utils/use_tracked_promise'; import { GetLogAlertsChartPreviewDataSuccessResponsePayload, @@ -22,11 +23,16 @@ interface Options { sourceId: string; ruleParams: GetLogAlertsChartPreviewDataAlertParamsSubset; buckets: number; + executionTimeRange?: ExecutionTimeRange; } -export const useChartPreviewData = ({ sourceId, ruleParams, buckets }: Options) => { +export const useChartPreviewData = ({ + sourceId, + ruleParams, + buckets, + executionTimeRange, +}: Options) => { const { http } = useKibana().services; - const [chartPreviewData, setChartPreviewData] = useState< GetLogAlertsChartPreviewDataSuccessResponsePayload['data']['series'] >([]); @@ -36,7 +42,13 @@ export const useChartPreviewData = ({ sourceId, ruleParams, buckets }: Options) cancelPreviousOn: 'creation', createPromise: async () => { setHasError(false); - return await callGetChartPreviewDataAPI(sourceId, http!.fetch, ruleParams, buckets); + return await callGetChartPreviewDataAPI( + sourceId, + http!.fetch, + ruleParams, + buckets, + executionTimeRange + ); }, onResolve: ({ data: { series } }) => { setHasError(false); @@ -66,7 +78,8 @@ export const callGetChartPreviewDataAPI = async ( sourceId: string, fetch: HttpHandler, alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, - buckets: number + buckets: number, + executionTimeRange?: ExecutionTimeRange ) => { const response = await fetch(LOG_ALERTS_CHART_PREVIEW_DATA_PATH, { method: 'POST', @@ -76,6 +89,7 @@ export const callGetChartPreviewDataAPI = async ( logView: { type: 'log-view-reference', logViewId: sourceId }, alertParams, buckets, + executionTimeRange, }, }) ), diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx index fa32b92806dcd..84ad31764b68a 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import { lazy } from 'react'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID, PartialRuleParams, @@ -33,6 +34,7 @@ export function createLogThresholdRuleType( documentationUrl(docLinks) { return `${docLinks.links.observability.logsThreshold}`; }, + alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')), ruleParamsExpression, validate: validateExpression, defaultActionMessage: i18n.translate( diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 269425e580bbc..e3e8d1e1c4ba6 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -112,6 +112,11 @@ export interface LensOptions { breakdownSize: number; } +export interface ExecutionTimeRange { + gte: number; + lte: number; +} + type PropsOf = T extends React.ComponentType ? ComponentProps : never; type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any ? FirstArgument diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts index 4483ad30c0246..4455eb5f53657 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { + ExecutionTimeRange, GroupedSearchQueryResponse, GroupedSearchQueryResponseRT, isOptimizedGroupedSearchQueryResponse, @@ -35,7 +36,8 @@ export async function getChartPreviewData( resolvedLogView: ResolvedLogView, callWithRequest: KibanaFramework['callWithRequest'], alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, - buckets: number + buckets: number, + executionTimeRange?: ExecutionTimeRange ) { const { indices, timestampField, runtimeMappings } = resolvedLogView; const { groupBy, timeSize, timeUnit } = alertParams; @@ -47,11 +49,10 @@ export async function getChartPreviewData( timeSize: timeSize * buckets, }; - const executionTimestamp = Date.now(); const { rangeFilter } = buildFiltersFromCriteria( expandedAlertParams, timestampField, - executionTimestamp + executionTimeRange ); const query = isGrouped @@ -60,14 +61,14 @@ export async function getChartPreviewData( timestampField, indices, runtimeMappings, - executionTimestamp + executionTimeRange ) : getUngroupedESQuery( expandedAlertParams, timestampField, indices, runtimeMappings, - executionTimestamp + executionTimeRange ); if (!query) { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 37c2cf002573b..79a258c620baf 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -140,7 +140,9 @@ const baseRuleParams: Pick { ...baseRuleParams, criteria: positiveCriteria, }; - const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMERANGE); expect(filters.mustFilters).toEqual(expectedPositiveFilterClauses); }); @@ -182,14 +184,14 @@ describe('Log threshold executor', () => { ...baseRuleParams, criteria: negativeCriteria, }; - const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMERANGE); expect(filters.mustNotFilters).toEqual(expectedNegativeFilterClauses); }); test('Handles time range', () => { const ruleParams: RuleParams = { ...baseRuleParams, criteria: [] }; - const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMERANGE); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number'); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number'); expect(filters.rangeFilter.range[TIMESTAMP_FIELD].format).toBe('epoch_millis'); @@ -212,7 +214,7 @@ describe('Log threshold executor', () => { TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings, - EXECUTION_TIMESTAMP + EXECUTION_TIMERANGE ); expect(query).toEqual({ index: 'filebeat-*', @@ -264,7 +266,7 @@ describe('Log threshold executor', () => { TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings, - EXECUTION_TIMESTAMP + EXECUTION_TIMERANGE ); expect(query).toEqual({ @@ -344,7 +346,7 @@ describe('Log threshold executor', () => { TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings, - EXECUTION_TIMESTAMP + EXECUTION_TIMERANGE ); expect(query).toEqual({ diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 1d0470c244fd1..7812b55e78b11 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -46,6 +46,7 @@ import { RatioRuleParams, UngroupedSearchQueryResponse, UngroupedSearchQueryResponseRT, + ExecutionTimeRange, } from '../../../../common/alerting/logs/log_threshold'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { getLogsAppAlertUrl } from '../../../../common/formatters/alert_link'; @@ -346,20 +347,23 @@ const getESQuery = ( runtimeMappings: estypes.MappingRuntimeFields, executionTimestamp: number ) => { + const executionTimeRange = { + lte: executionTimestamp, + }; return hasGroupBy(alertParams) ? getGroupedESQuery( alertParams, timestampField, indexPattern, runtimeMappings, - executionTimestamp + executionTimeRange ) : getUngroupedESQuery( alertParams, timestampField, indexPattern, runtimeMappings, - executionTimestamp + executionTimeRange ); }; @@ -641,14 +645,14 @@ export const processGroupByRatioResults = ( export const buildFiltersFromCriteria = ( params: Pick & { criteria: CountCriteria }, timestampField: string, - executionTimestamp: number + executionTimeRange?: ExecutionTimeRange ) => { const { timeSize, timeUnit, criteria } = params; const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); const intervalAsMs = intervalAsSeconds * 1000; - const to = executionTimestamp; - const from = to - intervalAsMs; + const to = executionTimeRange?.lte || Date.now(); + const from = executionTimeRange?.gte || to - intervalAsMs; const positiveCriteria = criteria.filter((criterion) => positiveComparators.includes(criterion.comparator) @@ -699,7 +703,7 @@ export const getGroupedESQuery = ( timestampField: string, index: string, runtimeMappings: estypes.MappingRuntimeFields, - executionTimestamp: number + executionTimeRange?: ExecutionTimeRange ): estypes.SearchRequest | undefined => { // IMPORTANT: // For the group by scenario we need to account for users utilizing "less than" configurations @@ -721,7 +725,7 @@ export const getGroupedESQuery = ( const { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( params, timestampField, - executionTimestamp + executionTimeRange ); if (isOptimizableGroupedThreshold(comparator, value)) { @@ -812,12 +816,12 @@ export const getUngroupedESQuery = ( timestampField: string, index: string, runtimeMappings: estypes.MappingRuntimeFields, - executionTimestamp: number + executionTimeRange?: ExecutionTimeRange ): object => { const { rangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( params, timestampField, - executionTimestamp + executionTimeRange ); const body: estypes.SearchRequest['body'] = { diff --git a/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts index fbc530397f4e3..d04a9a9bf491a 100644 --- a/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts @@ -29,7 +29,7 @@ export const initGetLogAlertsChartPreviewDataRoute = ({ }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { - data: { logView, buckets, alertParams }, + data: { logView, buckets, alertParams, executionTimeRange }, } = request.body; const [, , { logViews }] = await getStartServices(); @@ -41,7 +41,8 @@ export const initGetLogAlertsChartPreviewDataRoute = ({ resolvedLogView, framework.callWithRequest, alertParams, - buckets + buckets, + executionTimeRange ); return response.ok({