Skip to content

Commit

Permalink
[ML] AIOps: Fix incomplete edge buckets for change point detection (e…
Browse files Browse the repository at this point in the history
…lastic#177579)

## Summary

Fixes elastic#170117

Change point embeddable next to the Lens embeddable:

<img width="1343" alt="image"
src="https://github.com/elastic/kibana/assets/5236598/c9e73688-dd60-4803-a09a-e09f267ee730">



Extends search bounds based on the aggregation interval to complete the
edge buckets.


### 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
  • Loading branch information
darnautov authored Feb 23, 2024
1 parent e6a6a47 commit 37a1920
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import { type DataViewField } from '@kbn/data-views-plugin/public';
import { startWith } from 'rxjs';
import type { Filter, Query } from '@kbn/es-query';
import { usePageUrlState } from '@kbn/ml-url-state';
import { useTimefilter, useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { useTimefilter } from '@kbn/ml-date-picker';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { type QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import { FilterQueryContextProvider } from '../../hooks/use_filters_query';
import { useFilterQueryUpdates } from '../../hooks/use_filters_query';
import { type ChangePointType, DEFAULT_AGG_FUNCTION } from './constants';
import {
createMergedEsQuery,
Expand Down Expand Up @@ -155,14 +155,14 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
const timefilter = useTimefilter();
const timeBuckets = useTimeBuckets();

const { searchBounds } = useFilterQueryUpdates();

const [resultFilters, setResultFilter] = useState<Filter[]>([]);
const [selectedChangePoints, setSelectedChangePoints] = useState<
Record<number, SelectedChangePoint[]>
>({});
const [bucketInterval, setBucketInterval] = useState<TimeBucketsInterval>();

const timeRange = useTimeRangeUpdates(true);

useEffect(function updateIntervalOnTimeBoundsChange() {
const timeUpdateSubscription = timefilter
.getTimeUpdate$()
Expand Down Expand Up @@ -267,14 +267,14 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
mergedQuery.bool!.filter.push({
range: {
[dataView.timeFieldName!]: {
from: timeRange.from,
to: timeRange.to,
from: searchBounds.min?.valueOf(),
to: searchBounds.max?.valueOf(),
},
},
});

return mergedQuery;
}, [resultFilters, resultQuery, uiSettings, dataView, timeRange]);
}, [resultFilters, resultQuery, uiSettings, dataView, searchBounds]);

if (!bucketInterval) return null;

Expand All @@ -295,7 +295,7 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {

return (
<ChangePointDetectionContext.Provider value={value}>
<FilterQueryContextProvider>{children}</FilterQueryContextProvider>
{children}
</ChangePointDetectionContext.Provider>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import { timeSeriesDataViewWarning } from '../../application/utils/time_series_dataview_check';
import { ReloadContextProvider } from '../../hooks/use_reload';
import { AIOPS_TELEMETRY_ID } from '../../../common/constants';
import { FilterQueryContextProvider } from '../../hooks/use_filters_query';

const localStorage = new Storage(window.localStorage);

Expand Down Expand Up @@ -97,11 +98,13 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
<PageHeader />
<EuiSpacer />
<ReloadContextProvider reload$={reload$}>
<ChangePointDetectionContextProvider>
<ChangePointDetectionControlsContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionControlsContextProvider>
</ChangePointDetectionContextProvider>
<FilterQueryContextProvider>
<ChangePointDetectionContextProvider>
<ChangePointDetectionControlsContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionControlsContextProvider>
</ChangePointDetectionContextProvider>
</FilterQueryContextProvider>
</ReloadContextProvider>
</DatePickerContextProvider>
</StorageContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ export const MiniChartPreview: FC<ChartComponentProps> = ({
id={`mini_changePointChart_${annotation.group ? annotation.group.value : annotation.label}`}
style={{ height: 80 }}
timeRange={timeRange}
noPadding
query={query}
filters={filters}
// @ts-ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
* 2.0.
*/

import moment from 'moment';
import { FilterStateStore, type TimeRange } from '@kbn/es-query';
import { FilterStateStore } from '@kbn/es-query';
import { type TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { getAbsoluteTimeRange } from '@kbn/data-plugin/common';
import { useMemo } from 'react';
import { useFilerQueryUpdates } from '../../hooks/use_filters_query';
import { useFilterQueryUpdates } from '../../hooks/use_filters_query';
import { fnOperationTypeMapping } from './constants';
import { useDataSource } from '../../hooks/use_data_source';
import { ChangePointAnnotation, FieldConfig } from './change_point_detection_context';
Expand All @@ -32,20 +30,7 @@ export const useCommonChartProps = ({
}): Partial<TypedLensByValueInput> => {
const { dataView } = useDataSource();

const { filters: resultFilters, query: resultQuery, timeRange } = useFilerQueryUpdates();

/**
* In order to correctly render annotations for change points at the edges,
* we need to adjust time bound based on the change point timestamp.
*/
const chartTimeRange = useMemo<TimeRange>(() => {
const absoluteTimeRange = getAbsoluteTimeRange(timeRange);

return {
from: moment.min(moment(absoluteTimeRange.from), moment(annotation.timestamp)).toISOString(),
to: moment.max(moment(absoluteTimeRange.to), moment(annotation.timestamp)).toISOString(),
};
}, [timeRange, annotation.timestamp]);
const { filters: resultFilters, query: resultQuery, searchBounds } = useFilterQueryUpdates();

const filters = useMemo(() => {
return [
Expand Down Expand Up @@ -230,8 +215,13 @@ export const useCommonChartProps = ({
gridAndLabelsVisibility,
]);

const boundsTimeRange = {
from: searchBounds.min?.toISOString()!,
to: searchBounds.max?.toISOString()!,
};

return {
timeRange: chartTimeRange,
timeRange: boundsTimeRange,
filters,
query: resultQuery,
attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import { BehaviorSubject, combineLatest, type Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTimefilter } from '@kbn/ml-date-picker';
import { css } from '@emotion/react';
import useObservable from 'react-use/lib/useObservable';
import { ChangePointsTable } from '../components/change_point_detection/change_points_table';
Expand All @@ -24,10 +23,9 @@ import type {
EmbeddableChangePointChartOutput,
} from './embeddable_change_point_chart';
import { EmbeddableChangePointChartProps } from './embeddable_change_point_chart_component';
import { FilterQueryContextProvider, useFilerQueryUpdates } from '../hooks/use_filters_query';
import { FilterQueryContextProvider, useFilterQueryUpdates } from '../hooks/use_filters_query';
import { DataSourceContextProvider, useDataSource } from '../hooks/use_data_source';
import { useAiopsAppContext } from '../hooks/use_aiops_app_context';
import { useTimeBuckets } from '../hooks/use_time_buckets';
import { createMergedEsQuery } from '../application/utils/search_utils';
import { useChangePointResults } from '../components/change_point_detection/use_change_point_agg_request';
import { ChartsGrid } from '../components/change_point_detection/charts_grid';
Expand Down Expand Up @@ -85,8 +83,8 @@ export const EmbeddableInputTracker: FC<EmbeddableInputTrackerProps> = ({
return (
<ReloadContextProvider reload$={resultObservable$}>
<DataSourceContextProvider dataViewId={input.dataViewId}>
<ChangePointDetectionControlsContextProvider>
<FilterQueryContextProvider timeRange={input.timeRange}>
<FilterQueryContextProvider timeRange={input.timeRange}>
<ChangePointDetectionControlsContextProvider>
<ChartGridEmbeddableWrapper
viewType={input.viewType}
timeRange={input.timeRange}
Expand All @@ -102,8 +100,8 @@ export const EmbeddableInputTracker: FC<EmbeddableInputTrackerProps> = ({
onChange={input.onChange}
emptyState={input.emptyState}
/>
</FilterQueryContextProvider>
</ChangePointDetectionControlsContextProvider>
</ChangePointDetectionControlsContextProvider>
</FilterQueryContextProvider>
</DataSourceContextProvider>
</ReloadContextProvider>
);
Expand Down Expand Up @@ -139,22 +137,14 @@ export const ChartGridEmbeddableWrapper: FC<
onChange,
emptyState,
}) => {
const { filters, query, timeRange } = useFilerQueryUpdates();
const { filters, query, searchBounds, interval } = useFilterQueryUpdates();

const fieldConfig = useMemo(() => {
return { fn, metricField, splitField };
}, [fn, metricField, splitField]);

const { dataView } = useDataSource();
const { uiSettings } = useAiopsAppContext();
const timeBuckets = useTimeBuckets();
const timefilter = useTimefilter();

const interval = useMemo(() => {
timeBuckets.setInterval('auto');
timeBuckets.setBounds(timefilter.calculateBounds(timeRange));
return timeBuckets.getInterval().expression;
}, [timeRange, timeBuckets, timefilter]);

const combinedQuery = useMemo(() => {
const mergedQuery = createMergedEsQuery(query, filters, dataView, uiSettings);
Expand All @@ -168,8 +158,9 @@ export const ChartGridEmbeddableWrapper: FC<
mergedQuery.bool!.filter.push({
range: {
[dataView.timeFieldName!]: {
from: timeRange.from,
to: timeRange.to,
from: searchBounds.min?.valueOf(),
to: searchBounds.max?.valueOf(),
format: 'epoch_millis',
},
},
});
Expand All @@ -183,16 +174,7 @@ export const ChartGridEmbeddableWrapper: FC<
}

return mergedQuery;
}, [
dataView,
fieldConfig.splitField,
filters,
partitions,
query,
timeRange.from,
timeRange.to,
uiSettings,
]);
}, [dataView, fieldConfig.splitField, filters, partitions, query, searchBounds, uiSettings]);

const requestParams = useMemo<ChangePointDetectionRequestParams>(() => {
return { interval } as ChangePointDetectionRequestParams;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';

const staticContext = {
data: dataPluginMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
};

jest.spyOn(staticContext.uiSettings, 'get').mockImplementation((key: string) => {
if (key === 'dateFormat') {
return 'MMM D, YYYY @ HH:mm:ss.SSS';
}
if (key === 'dateFormat:scaled') {
return [
['', 'HH:mm:ss.SSS'],
['PT1S', 'HH:mm:ss'],
['PT1M', 'HH:mm'],
['PT1H', 'YYYY-MM-DD HH:mm'],
['P1DT', 'YYYY-MM-DD'],
['P1YT', 'YYYY'],
];
}
if (key === 'histogram:maxBars') {
return 1000;
}
if (key === 'histogram:barTarget') {
return 50;
}
return '';
});

export const useAiopsAppContext = jest.fn(() => {
return staticContext;
});
14 changes: 14 additions & 0 deletions x-pack/plugins/aiops/public/hooks/__mocks__/use_reload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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.
*/

const staticMock = {
refreshTimestamp: 1708711043761,
};

export const useReload = jest.fn(() => {
return staticMock;
});
Loading

0 comments on commit 37a1920

Please sign in to comment.