mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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 <img width="1191" alt="Screenshot 2023-03-23 at 13 10 09" src="https://user-images.githubusercontent.com/6838659/227239913-4516cf44-92f6-454c-a7af-72dafb12cfb6.png"> ### 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
This commit is contained in:
parent
378c5c198a
commit
c1a8b90f51
12 changed files with 147 additions and 27 deletions
|
@ -401,3 +401,8 @@ export const isOptimizableGroupedThreshold = (
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ExecutionTimeRange {
|
||||
gte?: number;
|
||||
lte: number;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -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 <x> <units>` 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
|
||||
<EuiFlexGroup>
|
||||
{rule.params.criteria.map((criteria) => {
|
||||
const chartCriterion = criteria as PartialCriterion;
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<CriterionPreview
|
||||
key={chartCriterion.field}
|
||||
ruleParams={rule.params}
|
||||
sourceId={rule.params.logView.logViewId}
|
||||
chartCriterion={chartCriterion}
|
||||
showThreshold={true}
|
||||
executionTimeRange={{ gte: rangeFrom, lte: rangeTo }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AlertDetailsAppSection;
|
|
@ -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<PartialRuleParams>;
|
||||
alert: TopAlert;
|
||||
}
|
|
@ -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<Criterion>;
|
||||
sourceId: string;
|
||||
showThreshold: boolean;
|
||||
executionTimeRange?: ExecutionTimeRange;
|
||||
}
|
||||
|
||||
export const CriterionPreview: React.FC<Props> = ({
|
||||
|
@ -63,6 +65,7 @@ export const CriterionPreview: React.FC<Props> = ({
|
|||
chartCriterion,
|
||||
sourceId,
|
||||
showThreshold,
|
||||
executionTimeRange,
|
||||
}) => {
|
||||
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
|
||||
const { field, comparator, value } = chartCriterion;
|
||||
|
@ -106,6 +109,7 @@ export const CriterionPreview: React.FC<Props> = ({
|
|||
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<ChartProps> = ({
|
||||
|
@ -124,6 +129,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
|
|||
threshold,
|
||||
chartAlertParams,
|
||||
showThreshold,
|
||||
executionTimeRange,
|
||||
}) => {
|
||||
const { uiSettings } = useKibana().services;
|
||||
const isDarkMode = uiSettings?.get('theme:darkMode') || false;
|
||||
|
@ -138,6 +144,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
|
|||
sourceId,
|
||||
ruleParams: chartAlertParams,
|
||||
buckets,
|
||||
executionTimeRange,
|
||||
});
|
||||
|
||||
useDebounce(
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -112,6 +112,11 @@ export interface LensOptions {
|
|||
breakdownSize: number;
|
||||
}
|
||||
|
||||
export interface ExecutionTimeRange {
|
||||
gte: number;
|
||||
lte: number;
|
||||
}
|
||||
|
||||
type PropsOf<T> = T extends React.ComponentType<infer ComponentProps> ? ComponentProps : never;
|
||||
type FirstArgumentOf<Func> = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any
|
||||
? FirstArgument
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -140,7 +140,9 @@ const baseRuleParams: Pick<RuleParams, 'count' | 'timeSize' | 'timeUnit' | 'logV
|
|||
|
||||
const TIMESTAMP_FIELD = '@timestamp';
|
||||
const FILEBEAT_INDEX = 'filebeat-*';
|
||||
const EXECUTION_TIMESTAMP = new Date('2022-01-01T00:00:00.000Z').valueOf();
|
||||
const EXECUTION_TIMERANGE = {
|
||||
lte: new Date('2022-01-01T00:00:00.000Z').valueOf(),
|
||||
};
|
||||
|
||||
const runtimeMappings: estypes.MappingRuntimeFields = {
|
||||
runtime_field: {
|
||||
|
@ -173,7 +175,7 @@ describe('Log threshold executor', () => {
|
|||
...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({
|
||||
|
|
|
@ -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<RuleParams, 'timeSize' | 'timeUnit'> & { 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'] = {
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue