Skip to content

Commit

Permalink
[AO] - Add scaffolding and the main chart to the Logs threshold Alert…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
fkanout authored Mar 28, 2023
1 parent 378c5c1 commit c1a8b90
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,8 @@ export const isOptimizableGroupedThreshold = (
return false;
}
};

export interface ExecutionTimeRange {
gte?: number;
lte: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
]),
}),
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -56,13 +57,15 @@ interface Props {
chartCriterion: Partial<Criterion>;
sourceId: string;
showThreshold: boolean;
executionTimeRange?: ExecutionTimeRange;
}

export const CriterionPreview: React.FC<Props> = ({
ruleParams,
chartCriterion,
sourceId,
showThreshold,
executionTimeRange,
}) => {
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
const { field, comparator, value } = chartCriterion;
Expand Down Expand Up @@ -106,6 +109,7 @@ export const CriterionPreview: React.FC<Props> = ({
threshold={ruleParams.count}
chartAlertParams={chartAlertParams}
showThreshold={showThreshold}
executionTimeRange={executionTimeRange}
/>
);
};
Expand All @@ -116,6 +120,7 @@ interface ChartProps {
threshold?: Threshold;
chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset;
showThreshold: boolean;
executionTimeRange?: ExecutionTimeRange;
}

const CriterionPreviewChart: React.FC<ChartProps> = ({
Expand All @@ -124,6 +129,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
threshold,
chartAlertParams,
showThreshold,
executionTimeRange,
}) => {
const { uiSettings } = useKibana().services;
const isDarkMode = uiSettings?.get('theme:darkMode') || false;
Expand All @@ -138,6 +144,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
sourceId,
ruleParams: chartAlertParams,
buckets,
executionTimeRange,
});

useDebounce(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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']
>([]);
Expand All @@ -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);
Expand Down Expand Up @@ -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',
Expand All @@ -76,6 +89,7 @@ export const callGetChartPreviewDataAPI = async (
logView: { type: 'log-view-reference', logViewId: sourceId },
alertParams,
buckets,
executionTimeRange,
},
})
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/infra/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { i18n } from '@kbn/i18n';
import {
ExecutionTimeRange,
GroupedSearchQueryResponse,
GroupedSearchQueryResponseRT,
isOptimizedGroupedSearchQueryResponse,
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -60,14 +61,14 @@ export async function getChartPreviewData(
timestampField,
indices,
runtimeMappings,
executionTimestamp
executionTimeRange
)
: getUngroupedESQuery(
expandedAlertParams,
timestampField,
indices,
runtimeMappings,
executionTimestamp
executionTimeRange
);

if (!query) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
});

Expand All @@ -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');
Expand All @@ -212,7 +214,7 @@ describe('Log threshold executor', () => {
TIMESTAMP_FIELD,
FILEBEAT_INDEX,
runtimeMappings,
EXECUTION_TIMESTAMP
EXECUTION_TIMERANGE
);
expect(query).toEqual({
index: 'filebeat-*',
Expand Down Expand Up @@ -264,7 +266,7 @@ describe('Log threshold executor', () => {
TIMESTAMP_FIELD,
FILEBEAT_INDEX,
runtimeMappings,
EXECUTION_TIMESTAMP
EXECUTION_TIMERANGE
);

expect(query).toEqual({
Expand Down Expand Up @@ -344,7 +346,7 @@ describe('Log threshold executor', () => {
TIMESTAMP_FIELD,
FILEBEAT_INDEX,
runtimeMappings,
EXECUTION_TIMESTAMP
EXECUTION_TIMERANGE
);

expect(query).toEqual({
Expand Down
Loading

0 comments on commit c1a8b90

Please sign in to comment.