Skip to content

Commit

Permalink
[ML] AIOps: Adds link to log rate analysis from anomaly table (elasti…
Browse files Browse the repository at this point in the history
…c#175289)

## Summary

Part of elastic#153753.

Adds an action to the actions menu of the ML anomaly table in both
Anomaly Explorer and Single Metric Viewer to Log Rate Analysis. The
action is available for the anomaly detection functions `count`,
`high_count`, `low_count`, `high_non_zero_count` and
`low_non_zero_count`.

- For multi-metric and population jobs, adds the current entity as a
Kuery search filter.
- Fixes an issue with restoring the correct time range from url state.
- Fixes an issue with restoring the correct query from url state.
- The action auto-selects an overall time range and baseline and
deviation time range based on the anomaly timestamp and multiples of
bucket spans.

Functional tests have been added that test the actions in both Anomaly
Explorer and Single Metric Viewer for single and multi metric `count`
job, as well as testing a job when the action should not be available.


[aiops-ad-link-0001.webm](https://github.com/elastic/kibana/assets/230104/c1d1336b-f62d-4861-b1a4-1a8ed9dbef4c)


### Checklist

- [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
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/4929
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
  • Loading branch information
walterra authored and CoenWarmer committed Feb 15, 2024
1 parent 0a03e29 commit f9ff946
Show file tree
Hide file tree
Showing 13 changed files with 554 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { cloneDeep } from 'lodash';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';

export function createCategorizeQuery(
queryIn: QueryDslQueryContainer,
queryIn: QueryDslQueryContainer | undefined,
timeField: string,
timeRange: { from: number; to: number } | undefined
) {
const query = cloneDeep(queryIn);
const query = cloneDeep(queryIn ?? { match_all: {} });

if (query.bool === undefined) {
query.bool = {};
Expand Down
22 changes: 22 additions & 0 deletions x-pack/plugins/aiops/public/application/url_state/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 { isDefaultSearchQuery } from './common';

describe('isDefaultSearchQuery', () => {
it('returns true for default search query', () => {
expect(isDefaultSearchQuery({ match_all: {} })).toBe(true);
});

it('returns false for non default search query', () => {
expect(
isDefaultSearchQuery({
bool: { must_not: [{ term: { 'the-term': 'the-value' } }] },
})
).toBe(false);
});
});
5 changes: 5 additions & 0 deletions x-pack/plugins/aiops/public/application/url_state/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

import type { Filter, Query } from '@kbn/es-query';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';

import { SEARCH_QUERY_LANGUAGE, type SearchQueryLanguage } from '@kbn/ml-query-utils';

const defaultSearchQuery = {
match_all: {},
} as const;

export const isDefaultSearchQuery = (arg: unknown): arg is typeof defaultSearchQuery => {
return isPopulatedObject(arg, ['match_all']);
};

export interface AiOpsPageUrlState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,7 @@ export function useDiscoverLinks() {
},
});

let path = basePath.get();
path += '/app/discover#/';
path += '?_g=' + _g;
path += '&_a=' + encodeURIComponent(_a);
const path = `${basePath.get()}/app/discover#/?_g=${_g}&_a=${encodeURIComponent(_a)}`;
window.open(path, '_blank');
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface LogRateAnalysisContentProps {
/** Optional callback that exposes data of the completed analysis */
onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void;
/** Optional callback that exposes current window parameters */
onWindowParametersChange?: (wp?: WindowParameters) => void;
onWindowParametersChange?: (wp?: WindowParameters, replace?: boolean) => void;
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
Expand Down Expand Up @@ -126,7 +126,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
windowParametersTouched.current = true;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,14 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {

useEffect(() => {
if (globalState?.time !== undefined) {
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
});
if (
!isEqual({ from: globalState.time.from, to: globalState.time.to }, timefilter.getTime())
) {
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(globalState?.time), timefilter]);
Expand All @@ -136,11 +140,14 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
});
}, [dataService, searchQueryLanguage, searchString]);

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

Expand Down
9 changes: 7 additions & 2 deletions x-pack/plugins/aiops/public/hooks/use_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,14 @@ export const useData = (
timeRangeSelector: selectedDataView?.timeFieldName !== undefined,
autoRefreshSelector: true,
});
const timeRangeMemoized = useMemo(
() => timefilter.getActiveBounds(),
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(timefilter.getActiveBounds())]
);

const fieldStatsRequest: DocumentStatsSearchStrategyParams | undefined = useMemo(() => {
const timefilterActiveBounds = timeRange ?? timefilter.getActiveBounds();
const timefilterActiveBounds = timeRange ?? timeRangeMemoized;
if (timefilterActiveBounds !== undefined) {
_timeBuckets.setInterval('auto');
_timeBuckets.setBounds(timefilterActiveBounds);
Expand All @@ -72,7 +77,7 @@ export const useData = (
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastRefresh, searchQuery, timeRange]);
}, [lastRefresh, searchQuery, timeRange, timeRangeMemoized]);

const overallStatsRequest = useMemo(() => {
return fieldStatsRequest
Expand Down
63 changes: 45 additions & 18 deletions x-pack/plugins/aiops/public/hooks/use_search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ import { useMemo } from 'react';

import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import { isQuery } from '@kbn/data-plugin/public';

import { getEsQueryFromSavedSearch } from '../application/utils/search_utils';
import type { AiOpsIndexBasedAppState } from '../application/url_state/common';
import {
isDefaultSearchQuery,
type AiOpsIndexBasedAppState,
} from '../application/url_state/common';
import { createMergedEsQuery } from '../application/utils/search_utils';

import { useAiopsAppContext } from './use_aiops_app_context';

export const useSearch = (
Expand All @@ -37,23 +43,44 @@ export const useSearch = (
[dataView, uiSettings, savedSearch, filterManager]
);

if (searchData === undefined || (aiopsListState && aiopsListState.searchString !== '')) {
if (aiopsListState?.filters && readOnly === false) {
const globalFilters = filterManager?.getGlobalFilters();
return useMemo(() => {
if (searchData === undefined || (aiopsListState && aiopsListState.searchString !== '')) {
if (aiopsListState?.filters && readOnly === false) {
const globalFilters = filterManager?.getGlobalFilters();

if (filterManager) filterManager.setFilters(aiopsListState.filters);
if (globalFilters) filterManager?.addFilters(globalFilters);
}

// In cases where the url state contains only a KQL query and not yet
// the transformed ES query we regenerate it. This may happen if we restore
// url state on page load coming from another page like ML's Single Metric Viewer.
let searchQuery = aiopsListState?.searchQuery;
const query = {
language: aiopsListState?.searchQueryLanguage,
query: aiopsListState?.searchString,
};
if (
(aiopsListState.searchString !== '' ||
(Array.isArray(aiopsListState.filters) && aiopsListState.filters.length > 0)) &&
(isDefaultSearchQuery(searchQuery) || searchQuery === undefined) &&
isQuery(query)
) {
searchQuery = createMergedEsQuery(query, aiopsListState.filters, dataView, uiSettings);
}

if (filterManager) filterManager.setFilters(aiopsListState.filters);
if (globalFilters) filterManager?.addFilters(globalFilters);
return {
...(isDefaultSearchQuery(searchQuery) ? {} : { searchQuery }),
searchString: aiopsListState?.searchString,
searchQueryLanguage: aiopsListState?.searchQueryLanguage,
};
} else {
return {
searchQuery: searchData.searchQuery,
searchString: searchData.searchString,
searchQueryLanguage: searchData.queryLanguage,
};
}
return {
searchQuery: aiopsListState?.searchQuery,
searchString: aiopsListState?.searchString,
searchQueryLanguage: aiopsListState?.searchQueryLanguage,
};
} else {
return {
searchQuery: searchData.searchQuery,
searchString: searchData.searchString,
searchQueryLanguage: searchData.queryLanguage,
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify([searchData, aiopsListState])]);
};
Loading

0 comments on commit f9ff946

Please sign in to comment.