Skip to content

Commit

Permalink
[ML] [AIOps] Log Rate Analysis: Adds support to restore baseline/devi…
Browse files Browse the repository at this point in the history
…ation from url state on page refresh. (#171398)

Support to restore baseline/deviation time ranges from url state on full
page refresh. Also updates functional tests to include a full page refresh after the
first analysis run for each dataset.
  • Loading branch information
walterra authored Nov 22, 2023
1 parent d5fc9b0 commit 19e97f3
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 53 deletions.
20 changes: 4 additions & 16 deletions x-pack/packages/ml/aiops_utils/window_parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,13 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object';
* @typedef {WindowParameters}
*/
export interface WindowParameters {
/**
* Baseline minimum value
* @type {number}
*/
/** Baseline minimum value */
baselineMin: number;
/**
* Baseline maximum value
* @type {number}
*/
/** Baseline maximum value */
baselineMax: number;
/**
* Deviation minimum value
* @type {number}
*/
/** Deviation minimum value */
deviationMin: number;
/**
* Deviation maximum value
* @type {number}
*/
/** Deviation maximum value */
deviationMax: number;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,3 @@ export const getDefaultAiOpsListState = (
filters: [],
...overrides,
});

export interface LogCategorizationPageUrlState {
pageKey: 'logCategorization';
pageUrlState: LogCategorizationAppState;
}

export interface LogCategorizationAppState extends AiOpsFullIndexBasedAppState {
field: string | undefined;
}

export const getDefaultLogCategorizationAppState = (
overrides?: Partial<LogCategorizationAppState>
): LogCategorizationAppState => {
return {
field: undefined,
...getDefaultAiOpsListState(overrides),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { getDefaultAiOpsListState, type AiOpsFullIndexBasedAppState } from './common';

export interface LogCategorizationPageUrlState {
pageKey: 'logCategorization';
pageUrlState: LogCategorizationAppState;
}

export interface LogCategorizationAppState extends AiOpsFullIndexBasedAppState {
field: string | undefined;
}

export const getDefaultLogCategorizationAppState = (
overrides?: Partial<LogCategorizationAppState>
): LogCategorizationAppState => {
return {
field: undefined,
...getDefaultAiOpsListState(overrides),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { WindowParameters } from '@kbn/aiops-utils';

import { getDefaultAiOpsListState, type AiOpsFullIndexBasedAppState } from './common';

export interface LogRateAnalysisPageUrlState {
pageKey: 'logRateAnalysis';
pageUrlState: LogRateAnalysisAppState;
}
/**
* To avoid long urls, we store the window parameters in the url state not with
* their full parameters names but with abbrevations. `windowParametersToAppState` and
* `appStateToWindowParameters` are used to transform the data structure.
*/
export interface LogRateAnalysisAppState extends AiOpsFullIndexBasedAppState {
/** Window parameters */
wp?: {
/** Baseline minimum value */
bMin: number;
/** Baseline maximum value */
bMax: number;
/** Deviation minimum value */
dMin: number;
/** Deviation maximum value */
dMax: number;
};
}

/**
* Transforms a full window parameters object to the abbreviated url state version.
*/
export const windowParametersToAppState = (wp?: WindowParameters): LogRateAnalysisAppState['wp'] =>
wp && {
bMin: wp.baselineMin,
bMax: wp.baselineMax,
dMin: wp.deviationMin,
dMax: wp.deviationMax,
};

/**
* Transforms an abbreviated url state version of window parameters to its full version.
*/
export const appStateToWindowParameters = (
wp: LogRateAnalysisAppState['wp']
): WindowParameters | undefined =>
wp && {
baselineMin: wp.bMin,
baselineMax: wp.bMax,
deviationMin: wp.dMin,
deviationMax: wp.dMax,
};

export const getDefaultLogRateAnalysisAppState = (
overrides?: Partial<LogRateAnalysisAppState>
): LogRateAnalysisAppState => {
return {
wp: undefined,
...getDefaultAiOpsListState(overrides),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import type {
} from '../../../../common/api/log_categorization/types';

import { useEuiTheme } from '../../../hooks/use_eui_theme';
import type { LogCategorizationAppState } from '../../../application/utils/url_state';
import type { LogCategorizationAppState } from '../../../application/url_state/log_pattern_analysis';

import { MiniHistogram } from '../../mini_histogram';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type { Category, SparkLinesPerCategory } from '../../../common/api/log_ca
import {
type LogCategorizationPageUrlState,
getDefaultLogCategorizationAppState,
} from '../../application/utils/url_state';
} from '../../application/url_state/log_pattern_analysis';
import { createMergedEsQuery } from '../../application/utils/search_utils';
import { useData } from '../../hooks/use_data';
import { useSearch } from '../../hooks/use_search';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import {
getDefaultLogCategorizationAppState,
type LogCategorizationPageUrlState,
} from '../../application/utils/url_state';
} from '../../application/url_state/log_pattern_analysis';

import { SearchPanel } from '../search_panel';
import { PageHeader } from '../page_header';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { Filter } from '@kbn/es-query';
import { getCategoryQuery } from '../../../common/api/log_categorization/get_category_query';
import type { Category } from '../../../common/api/log_categorization/types';

import type { AiOpsIndexBasedAppState } from '../../application/utils/url_state';
import type { AiOpsIndexBasedAppState } from '../../application/url_state/common';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';

export const QUERY_MODE = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { isEqual } from 'lodash';
import React, { useEffect, useMemo, useState, type FC } from 'react';
import React, { useEffect, useMemo, useRef, useState, type FC } from 'react';
import { EuiEmptyPrompt, EuiHorizontalRule, EuiPanel } from '@elastic/eui';
import type { Moment } from 'moment';

Expand Down Expand Up @@ -76,6 +76,8 @@ export interface LogRateAnalysisContentProps {
barHighlightColorOverride?: string;
/** Optional callback that exposes data of the completed analysis */
onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void;
/** Optional callback that exposes current window parameters */
onWindowParametersChange?: (wp?: WindowParameters) => void;
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
Expand All @@ -90,6 +92,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
barColorOverride,
barHighlightColorOverride,
onAnalysisCompleted,
onWindowParametersChange,
embeddingOrigin,
}) => {
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
Expand All @@ -105,6 +108,28 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
setIsBrushCleared(windowParameters === undefined);
}, [windowParameters]);

// Window parameters stored in the url state use this components
// `initialAnalysisStart` prop to set the initial params restore from url state.
// To avoid a loop with window parameters being passed around on load,
// the following ref and useEffect are used to check wether it's safe to call
// the `onWindowParametersChange` callback.
const windowParametersTouched = useRef(false);
useEffect(() => {
// Don't continue if window parameters were not touched yet.
// Because they can be reset to `undefined` at a later stage again when a user
// clears the selections, we cannot rely solely on checking if they are
// `undefined`, we need the additional ref to update on the first change.
if (!windowParametersTouched.current && windowParameters === undefined) {
return;
}

windowParametersTouched.current = true;

if (onWindowParametersChange) {
onWindowParametersChange(windowParameters);
}
}, [onWindowParametersChange, windowParameters]);

// Checks if `esSearchQuery` is the default empty query passed on from the search bar
// and if that's the case fall back to a simpler match all query.
const searchQuery = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@
*/

import React, { useCallback, useEffect, useState, FC } from 'react';
import { isEqual } from 'lodash';

import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { EuiFlexGroup, EuiFlexItem, EuiPageBody, EuiPageSection, EuiSpacer } from '@elastic/eui';

import { Filter, FilterStateStore, Query } from '@kbn/es-query';
import { useUrlState, usePageUrlState } from '@kbn/ml-url-state';

import type { SearchQueryLanguage } from '@kbn/ml-query-utils';
import type { WindowParameters } from '@kbn/aiops-utils';

import { useDataSource } from '../../hooks/use_data_source';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useData } from '../../hooks/use_data';
import { useSearch } from '../../hooks/use_search';
import {
getDefaultAiOpsListState,
type AiOpsPageUrlState,
} from '../../application/utils/url_state';
getDefaultLogRateAnalysisAppState,
appStateToWindowParameters,
windowParametersToAppState,
type LogRateAnalysisPageUrlState,
} from '../../application/url_state/log_rate_analysis';
import { AIOPS_TELEMETRY_ID } from '../../../common/constants';

import { SearchPanel } from '../search_panel';
Expand All @@ -40,9 +44,9 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
const { currentSelectedSignificantItem, currentSelectedGroup } =
useLogRateAnalysisResultsTableRowContext();

const [aiopsListState, setAiopsListState] = usePageUrlState<AiOpsPageUrlState>(
'AIOPS_INDEX_VIEWER',
getDefaultAiOpsListState()
const [stateFromUrl, setUrlState] = usePageUrlState<LogRateAnalysisPageUrlState>(
'logRateAnalysis',
getDefaultLogRateAnalysisAppState()
);
const [globalState, setGlobalState] = useUrlState('_g');

Expand All @@ -67,20 +71,20 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
setSelectedSavedSearch(null);
}

setAiopsListState({
...aiopsListState,
setUrlState({
...stateFromUrl,
searchQuery: searchParams.searchQuery,
searchString: searchParams.searchString,
searchQueryLanguage: searchParams.queryLanguage,
filters: searchParams.filters,
});
},
[selectedSavedSearch, aiopsListState, setAiopsListState]
[selectedSavedSearch, stateFromUrl, setUrlState]
);

const { searchQueryLanguage, searchString, searchQuery } = useSearch(
{ dataView, savedSearch },
aiopsListState
stateFromUrl
);

const { timefilter } = useData(
Expand Down Expand Up @@ -132,6 +136,14 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
});
}, [dataService, searchQueryLanguage, searchString]);

const onWindowParametersHandler = (wp?: WindowParameters) => {
if (!isEqual(wp, stateFromUrl.wp)) {
setUrlState({
wp: windowParametersToAppState(wp),
});
}
};

return (
<EuiPageBody data-test-subj="aiopsLogRateAnalysisPage" paddingSize="none" panelled={false}>
<PageHeader />
Expand All @@ -148,11 +160,13 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
/>
</EuiFlexItem>
<LogRateAnalysisContent
initialAnalysisStart={appStateToWindowParameters(stateFromUrl.wp)}
dataView={dataView}
setGlobalState={setGlobalState}
embeddingOrigin={AIOPS_TELEMETRY_ID.AIOPS_DEFAULT_SOURCE}
esSearchQuery={searchQuery}
onWindowParametersChange={onWindowParametersHandler}
setGlobalState={setGlobalState}
stickyHistogram={stickyHistogram}
embeddingOrigin={AIOPS_TELEMETRY_ID.AIOPS_DEFAULT_SOURCE}
/>
</EuiFlexGroup>
</EuiPageSection>
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/aiops/public/hooks/use_search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';

import { getEsQueryFromSavedSearch } from '../application/utils/search_utils';
import type { AiOpsIndexBasedAppState } from '../application/utils/url_state';
import type { AiOpsIndexBasedAppState } from '../application/url_state/common';
import { useAiopsAppContext } from './use_aiops_app_context';

export const useSearch = (
Expand Down
7 changes: 7 additions & 0 deletions x-pack/test/functional/apps/aiops/log_rate_analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { logRateAnalysisTestData } from './log_rate_analysis_test_data';

export default function ({ getPageObjects, getService }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'console', 'header', 'home', 'security']);
const browser = getService('browser');
const elasticChart = getService('elasticChart');
const aiops = getService('aiops');

Expand Down Expand Up @@ -147,6 +148,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await aiops.logRateAnalysisPage.clickRerunAnalysisButton(true);
}

// Wait for the analysis to finish
await aiops.logRateAnalysisPage.assertAnalysisComplete(testData.analysisType);

// At this stage the baseline and deviation brush position should be stored in
// the url state and a full browser refresh should restore the analysis.
await browser.refresh();
await aiops.logRateAnalysisPage.assertAnalysisComplete(testData.analysisType);

// The group switch should be disabled by default
Expand Down

0 comments on commit 19e97f3

Please sign in to comment.