[ML] Add option for anomaly charts for metric detector should plot min, mean or max as appropriate (#81662)

This commit is contained in:
Quynh Nguyen 2020-11-09 10:41:04 -06:00 committed by GitHub
parent 2c05957582
commit 9c984f4723
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 333 additions and 34 deletions

View file

@ -38,6 +38,7 @@ import {
import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact';
import { formatValue } from '../../formatters/format_value';
import { MAX_CHARS } from './anomalies_table_constants';
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
const TIME_FIELD_NAME = 'timestamp';
@ -130,7 +131,8 @@ function getDetailsItems(anomaly, examples, filter) {
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.functionTitle', {
defaultMessage: 'function',
}),
description: source.function !== 'metric' ? source.function : source.function_description,
description:
source.function !== ML_JOB_AGGREGATION.METRIC ? source.function : source.function_description,
});
if (source.field_name !== undefined) {

View file

@ -13,6 +13,8 @@ import { parseInterval } from '../../../../common/util/parse_interval';
import { getEntityFieldList } from '../../../../common/util/anomaly_utils';
import { buildConfigFromDetector } from '../../util/chart_config_builder';
import { mlJobService } from '../../services/job_service';
import { mlFunctionToESAggregation } from '../../../../common/util/job_utils';
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
// Builds the chart configuration for the provided anomaly record, returning
// an object with properties used for the display (series function and field, aggregation interval etc),
@ -48,6 +50,10 @@ export function buildConfig(record) {
// define the metric series to be plotted.
config.entityFields = getEntityFieldList(record);
if (record.function === ML_JOB_AGGREGATION.METRIC) {
config.metricFunction = mlFunctionToESAggregation(record.function_description);
}
// Build the tooltip data for the chart info icon, showing further details on what is being plotted.
let functionLabel = config.metricFunction;
if (config.metricFieldName !== undefined) {

View file

@ -46,8 +46,6 @@ const ML_TIME_FIELD_NAME = 'timestamp';
const USE_OVERALL_CHART_LIMITS = false;
const MAX_CHARTS_PER_ROW = 4;
// callback(getDefaultChartsData());
export const anomalyDataChange = function (
chartsContainerWidth,
anomalyRecords,

View file

@ -38,6 +38,7 @@ import { useResolver } from '../use_resolver';
import { basicResolvers } from '../resolvers';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { useTimefilter } from '../../contexts/kibana';
import { useToastNotificationService } from '../../services/toast_notification_service';
export const timeSeriesExplorerRouteFactory = (
navigateToPath: NavigateToPath,
@ -88,6 +89,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
config,
jobsWithTimeRange,
}) => {
const toastNotificationService = useToastNotificationService();
const [appState, setAppState] = useUrlState('_a');
const [globalState, setGlobalState] = useUrlState('_g');
const [lastRefresh, setLastRefresh] = useState(0);
@ -293,6 +295,7 @@ export const TimeSeriesExplorerUrlStateManager: FC<TimeSeriesExplorerUrlStateMan
return (
<TimeSeriesExplorer
{...{
toastNotificationService,
appStateHandler,
autoZoomDuration,
bounds,

View file

@ -24,8 +24,9 @@ export const resultsApiProvider = (httpService: HttpService) => ({
latestMs: number,
dateFormatTz: string,
maxRecords: number,
maxExamples: number,
influencersFilterQuery: any
maxExamples?: number,
influencersFilterQuery?: any,
functionDescription?: string
) {
const body = JSON.stringify({
jobIds,
@ -39,6 +40,7 @@ export const resultsApiProvider = (httpService: HttpService) => ({
maxRecords,
maxExamples,
influencersFilterQuery,
functionDescription,
});
return httpService.http$<any>({

View file

@ -19,6 +19,7 @@ import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
import { JobId } from '../../../../common/types/anomaly_detection_jobs';
import { MlApiServices } from '../ml_api_service';
import { CriteriaField } from './index';
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
interface ResultResponse {
success: boolean;
@ -347,9 +348,10 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
jobIds: string[],
criteriaFields: CriteriaField[],
threshold: any,
earliestMs: number,
latestMs: number,
maxResults: number | undefined
earliestMs: number | null,
latestMs: number | null,
maxResults: number | undefined,
functionDescription?: string
): Observable<RecordsForCriteria> {
const obj: RecordsForCriteria = { success: true, records: [] };
@ -400,6 +402,19 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) {
});
});
if (functionDescription !== undefined) {
const mlFunctionToPlotIfMetric =
functionDescription !== undefined
? aggregationTypeTransform.toML(functionDescription)
: functionDescription;
boolCriteria.push({
term: {
function_description: mlFunctionToPlotIfMetric,
},
});
}
return mlApiServices.results
.anomalySearch$(
{

View file

@ -76,6 +76,7 @@ export function resultsServiceProvider(
criteriaFields: any[],
earliestMs: number,
latestMs: number,
intervalMs: number
intervalMs: number,
actualPlotFunctionIfMetric?: string
): Promise<any>;
};

View file

@ -12,6 +12,7 @@ import {
ANOMALY_SWIM_LANE_HARD_LIMIT,
SWIM_LANE_DEFAULT_PAGE_SIZE,
} from '../../explorer/explorer_constants';
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
/**
* Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards.
@ -1293,7 +1294,14 @@ export function resultsServiceProvider(mlApiServices) {
// criteria, time range, and aggregation interval.
// criteriaFields parameter must be an array, with each object in the array having 'fieldName'
// 'fieldValue' properties.
getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, intervalMs) {
getRecordMaxScoreByTime(
jobId,
criteriaFields,
earliestMs,
latestMs,
intervalMs,
actualPlotFunctionIfMetric
) {
return new Promise((resolve, reject) => {
const obj = {
success: true,
@ -1321,7 +1329,18 @@ export function resultsServiceProvider(mlApiServices) {
},
});
});
if (actualPlotFunctionIfMetric !== undefined) {
const mlFunctionToPlotIfMetric =
actualPlotFunctionIfMetric !== undefined
? aggregationTypeTransform.toML(actualPlotFunctionIfMetric)
: actualPlotFunctionIfMetric;
mustCriteria.push({
term: {
function_description: mlFunctionToPlotIfMetric,
},
});
}
mlApiServices.results
.anomalySearch(
{

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { PlotByFunctionControls } from './plot_function_controls';

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const plotByFunctionOptions = [
{
value: 'mean',
text: i18n.translate('xpack.ml.timeSeriesExplorer.plotByAvgOptionLabel', {
defaultMessage: 'mean',
}),
},
{
value: 'min',
text: i18n.translate('xpack.ml.timeSeriesExplorer.plotByMinOptionLabel', {
defaultMessage: 'min',
}),
},
{
value: 'max',
text: i18n.translate('xpack.ml.timeSeriesExplorer.plotByMaxOptionLabel', {
defaultMessage: 'max',
}),
},
];
export const PlotByFunctionControls = ({
functionDescription,
setFunctionDescription,
}: {
functionDescription: undefined | string;
setFunctionDescription: (func: string) => void;
}) => {
if (functionDescription === undefined) return null;
return (
<EuiFlexItem grow={false}>
<EuiFormRow
label={i18n.translate('xpack.ml.timeSeriesExplorer.metricPlotByOption', {
defaultMessage: 'Function',
})}
>
<EuiSelect
options={plotByFunctionOptions}
value={functionDescription}
onChange={(e) => setFunctionDescription(e.target.value)}
aria-label={i18n.translate('xpack.ml.timeSeriesExplorer.metricPlotByOptionLabel', {
defaultMessage: 'Pick function to plot by (min, max, or average) if metric function',
})}
/>
</EuiFormRow>
</EuiFlexItem>
);
};

View file

@ -1475,6 +1475,22 @@ class TimeseriesChartIntl extends Component {
});
}
if (marker.metricFunction) {
tooltipData.push({
label: i18n.translate(
'xpack.ml.timeSeriesExplorer.timeSeriesChart.metricActualPlotFunctionLabel',
{
defaultMessage: 'function',
}
),
value: marker.metricFunction,
seriesIdentifier: {
key: seriesKey,
},
valueAccessor: 'metric_function',
});
}
if (modelPlotEnabled === false) {
// Show actual/typical when available except for rare detectors.
// Rare detectors always have 1 as actual and the probability as typical.

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/**
* Updates criteria fields for API calls, e.g. getAnomaliesTableData
* @param detectorIndex
* @param entities
*/
export const getCriteriaFields = (detectorIndex: number, entities: Record<string, any>) => {
// Only filter on the entity if the field has a value.
const nonBlankEntities = entities.filter(
(entity: { fieldValue: any }) => entity.fieldValue !== null
);
return [
{
fieldName: 'detector_index',
fieldValue: detectorIndex,
},
...nonBlankEntities,
];
};

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { mlResultsService } from '../services/results_service';
import { ToastNotificationService } from '../services/toast_notification_service';
import { getControlsForDetector } from './get_controls_for_detector';
import { getCriteriaFields } from './get_criteria_fields';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types';
/**
* Get the function description from the record with the highest anomaly score
*/
export const getFunctionDescription = async (
{
selectedDetectorIndex,
selectedEntities,
selectedJobId,
selectedJob,
}: {
selectedDetectorIndex: number;
selectedEntities: Record<string, any>;
selectedJobId: string;
selectedJob: CombinedJob;
},
toastNotificationService: ToastNotificationService
) => {
// if the detector's function is metric, fetch the highest scoring anomaly record
// and set to plot the function_description (avg/min/max) of that record by default
if (
selectedJob?.analysis_config?.detectors[selectedDetectorIndex]?.function !==
ML_JOB_AGGREGATION.METRIC
)
return;
const entityControls = getControlsForDetector(
selectedDetectorIndex,
selectedEntities,
selectedJobId
);
const criteriaFields = getCriteriaFields(selectedDetectorIndex, entityControls);
try {
const resp = await mlResultsService
.getRecordsForCriteria([selectedJob.job_id], criteriaFields, 0, null, null, 1)
.toPromise();
if (Array.isArray(resp?.records) && resp.records.length === 1) {
const highestScoringAnomaly = resp.records[0];
return highestScoringAnomaly?.function_description;
}
} catch (error) {
toastNotificationService.displayErrorToast(
error,
i18n.translate('xpack.ml.timeSeriesExplorer.highestAnomalyScoreErrorToastTitle', {
defaultMessage: 'An error occurred getting record with the highest anomaly score',
})
);
}
};

View file

@ -26,7 +26,8 @@ function getMetricData(
entityFields: EntityField[],
earliestMs: number,
latestMs: number,
intervalMs: number
intervalMs: number,
esMetricFunction?: string
): Observable<ModelPlotOutput> {
if (
isModelPlotChartableForDetector(job, detectorIndex) &&
@ -88,7 +89,7 @@ function getMetricData(
chartConfig.datafeedConfig.indices,
entityFields,
chartConfig.datafeedConfig.query,
chartConfig.metricFunction,
esMetricFunction ?? chartConfig.metricFunction,
chartConfig.metricFieldName,
chartConfig.timeField,
earliestMs,

View file

@ -82,6 +82,9 @@ import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/
import { getControlsForDetector } from './get_controls_for_detector';
import { SeriesControls } from './components/series_controls';
import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/timeseries_chart_with_tooltip';
import { PlotByFunctionControls } from './components/plot_function_controls';
import { aggregationTypeTransform } from '../../../common/util/anomaly_utils';
import { getFunctionDescription } from './get_function_description';
// Used to indicate the chart is being plotted across
// all partition field values, where the cardinality of the field cannot be
@ -140,6 +143,8 @@ function getTimeseriesexplorerDefaultState() {
zoomTo: undefined,
zoomFromFocusLoaded: undefined,
zoomToFocusLoaded: undefined,
// Sets function to plot by if original function is metric
functionDescription: undefined,
};
}
@ -217,6 +222,12 @@ export class TimeSeriesExplorer extends React.Component {
});
};
setFunctionDescription = (selectedFuction) => {
this.setState({
functionDescription: selectedFuction,
});
};
previousChartProps = {};
previousShowAnnotations = undefined;
previousShowForecast = undefined;
@ -270,7 +281,7 @@ export class TimeSeriesExplorer extends React.Component {
*/
getFocusData(selection) {
const { selectedJobId, selectedForecastId, selectedDetectorIndex } = this.props;
const { modelPlotEnabled } = this.state;
const { modelPlotEnabled, functionDescription } = this.state;
const selectedJob = mlJobService.getJob(selectedJobId);
const entityControls = this.getControlsForDetector();
@ -292,6 +303,7 @@ export class TimeSeriesExplorer extends React.Component {
entityControls.filter((entity) => entity.fieldValue !== null),
searchBounds,
selectedJob,
functionDescription,
TIME_FIELD_NAME
);
}
@ -322,6 +334,7 @@ export class TimeSeriesExplorer extends React.Component {
tableInterval,
tableSeverity,
} = this.props;
const { functionDescription } = this.state;
const selectedJob = mlJobService.getJob(selectedJobId);
const entityControls = this.getControlsForDetector();
@ -335,7 +348,10 @@ export class TimeSeriesExplorer extends React.Component {
earliestMs,
latestMs,
dateFormatTz,
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
undefined,
undefined,
functionDescription
)
.pipe(
map((resp) => {
@ -378,6 +394,24 @@ export class TimeSeriesExplorer extends React.Component {
);
};
getFunctionDescription = async () => {
const { selectedDetectorIndex, selectedEntities, selectedJobId } = this.props;
const selectedJob = mlJobService.getJob(selectedJobId);
const functionDescriptionToPlot = await getFunctionDescription(
{
selectedDetectorIndex,
selectedEntities,
selectedJobId,
selectedJob,
},
this.props.toastNotificationService
);
if (!this.unmounted) {
this.setFunctionDescription(functionDescriptionToPlot);
}
};
setForecastId = (forecastId) => {
this.props.appStateHandler(APP_STATE_ACTION.SET_FORECAST_ID, forecastId);
};
@ -392,13 +426,13 @@ export class TimeSeriesExplorer extends React.Component {
zoom,
} = this.props;
const { loadCounter: currentLoadCounter } = this.state;
const { loadCounter: currentLoadCounter, functionDescription } = this.state;
const currentSelectedJob = mlJobService.getJob(selectedJobId);
if (currentSelectedJob === undefined) {
return;
}
const functionToPlotByIfMetric = aggregationTypeTransform.toES(functionDescription);
this.contextChartSelectedInitCallDone = false;
@ -533,7 +567,8 @@ export class TimeSeriesExplorer extends React.Component {
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
stateUpdate.contextAggregationInterval.asMilliseconds()
stateUpdate.contextAggregationInterval.asMilliseconds(),
functionToPlotByIfMetric
)
.toPromise()
.then((resp) => {
@ -556,7 +591,8 @@ export class TimeSeriesExplorer extends React.Component {
this.getCriteriaFields(detectorIndex, entityControls),
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
stateUpdate.contextAggregationInterval.asMilliseconds()
stateUpdate.contextAggregationInterval.asMilliseconds(),
functionToPlotByIfMetric
)
.then((resp) => {
const fullRangeRecordScoreData = processRecordScoreResults(resp.results);
@ -687,7 +723,6 @@ export class TimeSeriesExplorer extends React.Component {
if (detectorId !== selectedDetectorIndex) {
appStateHandler(APP_STATE_ACTION.SET_DETECTOR_INDEX, detectorId);
}
// Populate the map of jobs / detectors / field formatters for the selected IDs and refresh.
mlFieldFormatService.populateFormats([jobId]).catch((err) => {
console.log('Error populating field formats:', err);
@ -810,7 +845,7 @@ export class TimeSeriesExplorer extends React.Component {
this.componentDidUpdate();
}
componentDidUpdate(previousProps) {
componentDidUpdate(previousProps, previousState) {
if (previousProps === undefined || previousProps.selectedJobId !== this.props.selectedJobId) {
this.contextChartSelectedInitCallDone = false;
this.setState({ fullRefresh: false, loading: true }, () => {
@ -818,6 +853,15 @@ export class TimeSeriesExplorer extends React.Component {
});
}
if (
previousProps === undefined ||
previousProps.selectedJobId !== this.props.selectedJobId ||
previousProps.selectedDetectorIndex !== this.props.selectedDetectorIndex ||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities)
) {
this.getFunctionDescription();
}
if (
previousProps === undefined ||
previousProps.selectedForecastId !== this.props.selectedForecastId
@ -840,7 +884,8 @@ export class TimeSeriesExplorer extends React.Component {
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
previousProps.selectedJobId !== this.props.selectedJobId
previousProps.selectedJobId !== this.props.selectedJobId ||
previousState.functionDescription !== this.state.functionDescription
) {
const fullRefresh =
previousProps === undefined ||
@ -848,7 +893,8 @@ export class TimeSeriesExplorer extends React.Component {
!isEqual(previousProps.selectedDetectorIndex, this.props.selectedDetectorIndex) ||
!isEqual(previousProps.selectedEntities, this.props.selectedEntities) ||
previousProps.selectedForecastId !== this.props.selectedForecastId ||
previousProps.selectedJobId !== this.props.selectedJobId;
previousProps.selectedJobId !== this.props.selectedJobId ||
previousState.functionDescription !== this.state.functionDescription;
this.loadSingleMetricData(fullRefresh);
}
@ -919,8 +965,8 @@ export class TimeSeriesExplorer extends React.Component {
zoomTo,
zoomFromFocusLoaded,
zoomToFocusLoaded,
functionDescription,
} = this.state;
const chartProps = {
modelPlotEnabled,
contextChartData,
@ -939,7 +985,6 @@ export class TimeSeriesExplorer extends React.Component {
zoomToFocusLoaded,
autoZoomDuration,
};
const jobs = createTimeSeriesJobData(mlJobService.jobs);
if (selectedDetectorIndex === undefined || mlJobService.getJob(selectedJobId) === undefined) {
@ -992,7 +1037,6 @@ export class TimeSeriesExplorer extends React.Component {
<EuiSpacer size="m" />
</>
)}
<SeriesControls
selectedJobId={selectedJobId}
appStateHandler={this.props.appStateHandler}
@ -1000,6 +1044,16 @@ export class TimeSeriesExplorer extends React.Component {
selectedEntities={this.props.selectedEntities}
bounds={bounds}
>
{functionDescription && (
<PlotByFunctionControls
selectedJobId={selectedJobId}
selectedDetectorIndex={selectedDetectorIndex}
selectedEntities={this.props.selectedEntities}
functionDescription={functionDescription}
setFunctionDescription={this.setFunctionDescription}
/>
)}
{arePartitioningFieldsProvided && (
<EuiFlexItem style={{ textAlign: 'right' }}>
<EuiFormRow hasEmptyLabelSpace style={{ maxWidth: '100%' }}>
@ -1014,7 +1068,6 @@ export class TimeSeriesExplorer extends React.Component {
</EuiFlexItem>
)}
</SeriesControls>
<EuiSpacer size="m" />
{fullRefresh && loading === true && (

View file

@ -26,6 +26,7 @@ import { mlForecastService } from '../../services/forecast_service';
import { mlFunctionToESAggregation } from '../../../../common/util/job_utils';
import { GetAnnotationsResponse } from '../../../../common/types/annotations';
import { ANNOTATION_EVENT_USER } from '../../../../common/constants/annotations';
import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils';
export interface Interval {
asMilliseconds: () => number;
@ -51,8 +52,14 @@ export function getFocusData(
modelPlotEnabled: boolean,
nonBlankEntities: any[],
searchBounds: any,
selectedJob: Job
selectedJob: Job,
functionDescription?: string | undefined
): Observable<FocusData> {
const esFunctionToPlotIfMetric =
functionDescription !== undefined
? aggregationTypeTransform.toES(functionDescription)
: functionDescription;
return forkJoin([
// Query 1 - load metric data across selected time range.
mlTimeSeriesSearchService.getMetricData(
@ -61,7 +68,8 @@ export function getFocusData(
nonBlankEntities,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
focusAggregationInterval.asMilliseconds()
focusAggregationInterval.asMilliseconds(),
esFunctionToPlotIfMetric
),
// Query 2 - load all the records across selected time range for the chart anomaly markers.
mlResultsService.getRecordsForCriteria(
@ -70,7 +78,8 @@ export function getFocusData(
0,
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
functionDescription
),
// Query 3 - load any scheduled events for the selected job.
mlResultsService.getScheduledEventsByBucket(
@ -143,7 +152,8 @@ export function getFocusData(
focusChartData,
anomalyRecords,
focusAggregationInterval,
modelPlotEnabled
modelPlotEnabled,
functionDescription
);
focusChartData = processScheduledEventsForChart(focusChartData, scheduledEvents);

View file

@ -16,7 +16,8 @@ export function processDataForFocusAnomalies(
chartData: any,
anomalyRecords: any,
aggregationInterval: any,
modelPlotEnabled: any
modelPlotEnabled: any,
functionDescription: any
): any;
export function processScheduledEventsForChart(chartData: any, scheduledEvents: any): any;

View file

@ -19,6 +19,7 @@ import { parseInterval } from '../../../../common/util/parse_interval';
import { getBoundsRoundedToInterval, getTimeBucketsFromCache } from '../../util/time_buckets';
import { CHARTS_POINT_TARGET, TIME_FIELD_NAME } from '../timeseriesexplorer_constants';
import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types';
// create new job objects based on standard job config objects
// new job objects just contain job id, bucket span in seconds and a selected flag.
@ -100,7 +101,8 @@ export function processDataForFocusAnomalies(
chartData,
anomalyRecords,
aggregationInterval,
modelPlotEnabled
modelPlotEnabled,
functionDescription
) {
const timesToAddPointsFor = [];
@ -142,6 +144,12 @@ export function processDataForFocusAnomalies(
// Look for a chart point with the same time as the record.
// If none found, find closest time in chartData set.
const recordTime = record[TIME_FIELD_NAME];
if (
record.function === ML_JOB_AGGREGATION.METRIC &&
record.function_description !== functionDescription
)
return;
const chartPoint = findChartPointForAnomalyTime(chartData, recordTime, aggregationInterval);
if (chartPoint !== undefined) {
// If chart aggregation interval > bucket span, there may be more than
@ -160,6 +168,10 @@ export function processDataForFocusAnomalies(
chartPoint.value = record.actual;
}
if (record.function === ML_JOB_AGGREGATION.METRIC) {
chartPoint.value = Array.isArray(record.actual) ? record.actual[0] : record.actual;
}
chartPoint.actual = record.actual;
chartPoint.typical = record.typical;
} else {

View file

@ -54,7 +54,8 @@ export function resultsServiceProvider(mlClient: MlClient) {
dateFormatTz: string,
maxRecords: number = ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
maxExamples: number = DEFAULT_MAX_EXAMPLES,
influencersFilterQuery: any
influencersFilterQuery?: any,
functionDescription?: string
) {
// Build the query to return the matching anomaly record results.
// Add criteria for the time range, record score, plus any specified job IDs.
@ -102,6 +103,13 @@ export function resultsServiceProvider(mlClient: MlClient) {
},
});
});
if (functionDescription !== undefined) {
boolCriteria.push({
term: {
function_description: functionDescription,
},
});
}
if (influencersFilterQuery !== undefined) {
boolCriteria.push(influencersFilterQuery);

View file

@ -36,6 +36,7 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) {
maxRecords,
maxExamples,
influencersFilterQuery,
functionDescription,
} = payload;
return rs.getAnomaliesTableData(
jobIds,
@ -48,7 +49,8 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) {
dateFormatTz,
maxRecords,
maxExamples,
influencersFilterQuery
influencersFilterQuery,
functionDescription
);
}

View file

@ -26,6 +26,7 @@ export const anomaliesTableDataSchema = schema.object({
maxRecords: schema.number(),
maxExamples: schema.maybe(schema.number()),
influencersFilterQuery: schema.maybe(schema.any()),
functionDescription: schema.maybe(schema.nullable(schema.string())),
});
export const categoryDefinitionSchema = schema.object({