function loadViewBySwimlaneForSelectedTime(earliestMs, latestMs) { const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); const swimlaneLimit = (limit === undefined) ? SWIMLANE_DEFAULT_LIMIT : limit.val; // Find the top field values for the selected time, and then load the 'view by' // swimlane over the full time range for those specific field values. if ($scope.swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { mlResultsService.getTopInfluencers( selectedJobIds, earliestMs, latestMs, swimlaneLimit ).then((resp) => { const topFieldValues = []; const topInfluencers = resp.influencers[$scope.swimlaneViewByFieldName]; _.each(topInfluencers, (influencerData) => { if (influencerData.maxAnomalyScore > 0) { topFieldValues.push(influencerData.influencerFieldValue); } }); loadViewBySwimlane(topFieldValues); }); } else { mlResultsService.getScoresByBucket( selectedJobIds, earliestMs, latestMs, $scope.swimlaneBucketInterval.asSeconds() + 's', swimlaneLimit ).then((resp) => { loadViewBySwimlane(_.keys(resp.results)); }); } }
return new Promise((resolve) => { // Just skip doing the request when this function // is called without the minimum required data. if (selectedCells === null && influencers.length === 0 && influencersFilterQuery === undefined) { resolve([]); } const newRequestCount = ++requestCount; requestCount = newRequestCount; // Load the top anomalies (by record_score) which will be displayed in the charts. mlResultsService.getRecordsForInfluencer( jobIds, influencers, 0, earliestMs, latestMs, 500, influencersFilterQuery ) .then((resp) => { // Ignore this response if it's returned by an out of date promise if (newRequestCount < requestCount) { resolve(undefined); } if ((selectedCells !== null && Object.keys(selectedCells).length > 0) || influencersFilterQuery !== undefined) { console.log('Explorer anomaly charts data set:', resp.records); resolve(resp.records); } resolve(undefined); }); });
function loadEntityValues() { // Populate the entity input datalists with the values from the top records by score // for the selected detector across the full time range. No need to pass through finish(). const bounds = timefilter.getActiveBounds(); const detectorIndex = +$scope.detectorId; mlResultsService.getRecordsForCriteria( [$scope.selectedJob.job_id], [{ 'fieldName': 'detector_index', 'fieldValue': detectorIndex }], 0, bounds.min.valueOf(), bounds.max.valueOf(), ANOMALIES_MAX_RESULTS) .then((resp) => { if (resp.records && resp.records.length > 0) { const firstRec = resp.records[0]; _.each($scope.entities, (entity) => { if (firstRec.partition_field_name === entity.fieldName) { entity.fieldValues = _.chain(resp.records).pluck('partition_field_value').uniq().value(); } if (firstRec.over_field_name === entity.fieldName) { entity.fieldValues = _.chain(resp.records).pluck('over_field_value').uniq().value(); } if (firstRec.by_field_name === entity.fieldName) { entity.fieldValues = _.chain(resp.records).pluck('by_field_value').uniq().value(); } }); } }); }
return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; const chartConfig = buildConfigFromDetector(job, detectorIndex); mlResultsService.getMetricData( chartConfig.datafeedConfig.indices, entityFields, chartConfig.datafeedConfig.query, chartConfig.metricFunction, chartConfig.metricFieldName, chartConfig.timeField, earliestMs, latestMs, interval ) .then((resp) => { _.each(resp.results, (value, time) => { obj.results[time] = { 'actual': value }; }); resolve(obj); }) .catch((resp) => { reject(resp); }); });
function loadOverallData() { // Loads the overall data components i.e. the overall swimlane and influencers list. if ($scope.selectedJobs === null) { return; } $scope.loading = true; $scope.hasResults = false; $scope.swimlaneBucketInterval = calculateSwimlaneBucketInterval(); console.log('Explorer swimlane bucketInterval:', $scope.swimlaneBucketInterval); // Ensure the search bounds align to the bucketing interval used in the swimlane so // that the first and last buckets are complete. const bounds = timefilter.getActiveBounds(); const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); const selectedJobIds = $scope.getSelectedJobIds(); // Load the overall bucket scores by time. // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets // which wouldn't be the case if e.g. '1M' was used. // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works // to ensure the search is inclusive of end time. const overallBucketsBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, true); mlResultsService.getOverallBucketScores( selectedJobIds, // Note there is an optimization for when top_n == 1. // If top_n > 1, we should test what happens when the request takes long // and refactor the loading calls, if necessary, to avoid delays in loading other components. 1, overallBucketsBounds.min.valueOf(), overallBucketsBounds.max.valueOf(), $scope.swimlaneBucketInterval.asSeconds() + 's' ).then((resp) => { skipCellClicks = false; $scope.overallSwimlaneData = processOverallResults(resp.results, searchBounds); console.log('Explorer overall swimlane data set:', $scope.overallSwimlaneData); if ($scope.overallSwimlaneData.points && $scope.overallSwimlaneData.points.length > 0) { $scope.hasResults = true; // Trigger loading of the 'view by' swimlane - // only load once the overall swimlane so that we can match the time span. setViewBySwimlaneOptions(); } else { $scope.hasResults = false; } $scope.loading = false; // Tell the result components directives to render. // Need to use $timeout to ensure the broadcast happens after the child scope is updated with the new data. $timeout(() => { loadViewBySwimlane([]); }, 0); }); }
return new Promise((resolve) => { if (_.isEqual(compareArgs, this.topFieldsPreviousArgs)) { resolve(this.topFieldsPreviousData); return; } this.topFieldsPreviousArgs = compareArgs; if (swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { mlResultsService.getTopInfluencers( selectedJobIds, earliestMs, latestMs, swimlaneLimit ).then((resp) => { if (resp.influencers[swimlaneViewByFieldName] === undefined) { this.topFieldsPreviousData = []; resolve([]); } const topFieldValues = []; const topInfluencers = resp.influencers[swimlaneViewByFieldName]; topInfluencers.forEach((influencerData) => { if (influencerData.maxAnomalyScore > 0) { topFieldValues.push(influencerData.influencerFieldValue); } }); this.topFieldsPreviousData = topFieldValues; resolve(topFieldValues); }); } else { mlResultsService.getScoresByBucket( selectedJobIds, earliestMs, latestMs, this.getSwimlaneBucketInterval(selectedJobs).asSeconds() + 's', swimlaneLimit ).then((resp) => { const topFieldValues = Object.keys(resp.results); this.topFieldsPreviousData = topFieldValues; resolve(topFieldValues); }); } });
return new Promise((resolve, reject) => { let start = formConfig.start; if (this.chartData.model.length > 5) { // only load the model since the end of the last time we checked // but discard the last 5 buckets in case the model has changed start = this.chartData.model[this.chartData.model.length - 5].time; for (let i = 0; i < 5; i++) { this.chartData.model.pop(); } } // Obtain the model plot data, passing 0 for the detectorIndex and empty list of partitioning fields. mlResultsService.getModelPlotOutput( formConfig.jobId, 0, [], start, formConfig.end, formConfig.resultsIntervalSeconds + 's', formConfig.agg.type.mlModelPlotAgg ) .then(data => { // for count, scale the model upper and lower by the // ratio of chart interval to bucketspan. // this will force the model bounds to be drawn in the correct location let scale = 1; if (formConfig && (formConfig.agg.type.mlName === 'count' || formConfig.agg.type.mlName === 'high_count' || formConfig.agg.type.mlName === 'low_count' || formConfig.agg.type.mlName === 'distinct_count')) { const chartIntervalSeconds = formConfig.chartInterval.getInterval().asSeconds(); const bucketSpan = parseInterval(formConfig.bucketSpan); if (bucketSpan !== null) { scale = chartIntervalSeconds / bucketSpan.asSeconds(); } } this.chartData.model = this.chartData.model.concat(processLineChartResults(data.results, scale)); const lastBucket = this.chartData.model[this.chartData.model.length - 1]; const time = (lastBucket !== undefined) ? lastBucket.time : formConfig.start; const pcnt = ((time - formConfig.start + formConfig.resultsIntervalSeconds) / (formConfig.end - formConfig.start) * 100); this.chartData.percentComplete = Math.round(pcnt); resolve(this.chartData); }) .catch(() => { reject(this.chartData); }); });
return new Promise((resolve) => { if (noInfluencersConfigured !== true) { mlResultsService.getTopInfluencers( selectedJobIds, earliestMs, latestMs, MAX_INFLUENCER_FIELD_VALUES, influencers, influencersFilterQuery ).then((resp) => { // TODO - sort the influencers keys so that the partition field(s) are first. console.log('Explorer top influencers data set:', resp.influencers); resolve(resp.influencers); }); } else { resolve({}); } });
function loadTopInfluencers(selectedJobIds, earliestMs, latestMs, influencers = []) { if ($scope.noInfluencersConfigured !== true) { mlResultsService.getTopInfluencers( selectedJobIds, earliestMs, latestMs, MAX_INFLUENCER_FIELD_VALUES, influencers ).then((resp) => { // TODO - sort the influencers keys so that the partition field(s) are first. $scope.influencers = resp.influencers; $scope.$applyAsync(); console.log('Explorer top influencers data set:', $scope.influencers); }); } else { $scope.influencers = {}; $scope.$applyAsync(); } }
return new Promise((resolve) => { mlResultsService.getScoresByBucket( [formConfig.jobId], formConfig.start, formConfig.end, formConfig.resultsIntervalSeconds + 's', 1 ) .then((data) => { const jobResults = data.results[formConfig.jobId]; this.chartData.swimlane = processSwimlaneResults(jobResults); this.chartData.swimlaneInterval = formConfig.resultsIntervalSeconds * 1000; resolve(this.chartData); }) .catch(() => { resolve(this.chartData); }); });
function loadDataForCharts(jobIds, earliestMs, latestMs, influencers = []) { // Just skip doing the request when this function // is called without the minimum required data. if ($scope.cellData === undefined && influencers.length === 0) { return; } const newRequestCount = ++requestCount; requestCount = newRequestCount; // Load the top anomalies (by record_score) which will be displayed in the charts. mlResultsService.getRecordsForInfluencer( jobIds, influencers, 0, earliestMs, latestMs, 500 ) .then((resp) => { // Ignore this response if it's returned by an out of date promise if (newRequestCount < requestCount) { return; } if ($scope.cellData !== undefined && _.keys($scope.cellData).length > 0) { $scope.anomalyChartRecords = resp.records; console.log('Explorer anomaly charts data set:', $scope.anomalyChartRecords); if (mlCheckboxShowChartsService.state.get('showCharts')) { anomalyDataChange( $scope.anomalyChartRecords, earliestMs, latestMs ); } } // While the charts were loaded, other events could reset cellData, // so check if it's still present. This can happen if a cell selection // gets restored from URL/AppState and we find out it's not applicable // to the view by swimlanes currently on display. if ($scope.cellData === undefined) { return; } if (influencers.length > 0) { // Filter the Top Influencers list to show just the influencers from // the records in the selected time range. const recordInfluencersByName = {}; // Add the specified influencer(s) to ensure they are used in the filter // even if their influencer score for the selected time range is zero. influencers.forEach((influencer) => { const fieldName = influencer.fieldName; if (recordInfluencersByName[influencer.fieldName] === undefined) { recordInfluencersByName[influencer.fieldName] = []; } recordInfluencersByName[fieldName].push(influencer.fieldValue); }); // Add the influencers from the top scoring anomalies. resp.records.forEach((record) => { const influencersByName = record.influencers || []; influencersByName.forEach((influencer) => { const fieldName = influencer.influencer_field_name; const fieldValues = influencer.influencer_field_values; if (recordInfluencersByName[fieldName] === undefined) { recordInfluencersByName[fieldName] = []; } recordInfluencersByName[fieldName].push(...fieldValues); }); }); const uniqValuesByName = {}; Object.keys(recordInfluencersByName).forEach((fieldName) => { const fieldValues = recordInfluencersByName[fieldName]; uniqValuesByName[fieldName] = _.uniq(fieldValues); }); const filterInfluencers = []; Object.keys(uniqValuesByName).forEach((fieldName) => { // Find record influencers with the same field name as the clicked on cell(s). const matchingFieldName = influencers.find((influencer) => { return influencer.fieldName === fieldName; }); if (matchingFieldName !== undefined) { // Filter for the value(s) of the clicked on cell(s). filterInfluencers.push(...influencers); } else { // For other field names, add values from all records. uniqValuesByName[fieldName].forEach((fieldValue) => { filterInfluencers.push({ fieldName, fieldValue }); }); } }); loadTopInfluencers(jobIds, earliestMs, latestMs, filterInfluencers); } $scope.$applyAsync(); }); }
$scope.refreshFocusData = function (fromDate, toDate) { // Counter to keep track of the queries to populate the chart. let awaitingCount = 3; // This object is used to store the results of individual remote requests // before we transform it into the final data and apply it to $scope. Otherwise // we might trigger multiple $digest cycles and depending on how deep $watches // listen for changes we could miss updates. const refreshFocusData = {}; // finish() function, called after each data set has been loaded and processed. // The last one to call it will trigger the page render. function finish() { awaitingCount--; if (awaitingCount === 0) { // Tell the results container directives to render the focus chart. refreshFocusData.focusChartData = processDataForFocusAnomalies( refreshFocusData.focusChartData, refreshFocusData.anomalyRecords, $scope.timeFieldName); refreshFocusData.focusChartData = processScheduledEventsForChart( refreshFocusData.focusChartData, refreshFocusData.scheduledEvents); // All the data is ready now for a scope update. // Use $evalAsync to ensure the update happens after the child scope is updated with the new data. $scope.$evalAsync(() => { $scope = Object.assign($scope, refreshFocusData); console.log('Time series explorer focus chart data set:', $scope.focusChartData); $scope.loading = false; }); } } const detectorIndex = +$scope.detectorId; const nonBlankEntities = _.filter($scope.entities, entity => entity.fieldValue.length > 0); // Calculate the aggregation interval for the focus chart. const bounds = { min: moment(fromDate), max: moment(toDate) }; $scope.focusAggregationInterval = calculateAggregationInterval(bounds, CHARTS_POINT_TARGET, CHARTS_POINT_TARGET); // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected // to some extent with all detector functions if not searching complete buckets. const searchBounds = getBoundsRoundedToInterval(bounds, $scope.focusAggregationInterval, false); // Query 1 - load metric data across selected time range. mlTimeSeriesSearchService.getMetricData( $scope.selectedJob, detectorIndex, nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), $scope.focusAggregationInterval.expression ).then((resp) => { refreshFocusData.focusChartData = processMetricPlotResults(resp.results, $scope.modelPlotEnabled); $scope.showModelBoundsCheckbox = ($scope.modelPlotEnabled === true) && (refreshFocusData.focusChartData.length > 0); finish(); }).catch((resp) => { console.log('Time series explorer - error getting metric data from elasticsearch:', resp); }); // Query 2 - load all the records across selected time range for the chart anomaly markers. mlResultsService.getRecordsForCriteria( [$scope.selectedJob.job_id], $scope.criteriaFields, 0, searchBounds.min.valueOf(), searchBounds.max.valueOf(), ANOMALIES_MAX_RESULTS ).then((resp) => { // Sort in descending time order before storing in scope. refreshFocusData.anomalyRecords = _.chain(resp.records) .sortBy(record => record[$scope.timeFieldName]) .reverse() .value(); console.log('Time series explorer anomalies:', refreshFocusData.anomalyRecords); finish(); }); // Query 3 - load any scheduled events for the selected job. mlResultsService.getScheduledEventsByBucket( [$scope.selectedJob.job_id], searchBounds.min.valueOf(), searchBounds.max.valueOf(), $scope.focusAggregationInterval.expression, 1, MAX_SCHEDULED_EVENTS ).then((resp) => { refreshFocusData.scheduledEvents = resp.events[$scope.selectedJob.job_id]; finish(); }).catch((resp) => { console.log('Time series explorer - error getting scheduled events from elasticsearch:', resp); }); // Plus query for forecast data if there is a forecastId stored in the appState. const forecastId = _.get($scope, 'appState.mlTimeSeriesExplorer.forecastId'); if (forecastId !== undefined) { awaitingCount++; let aggType = undefined; const detector = $scope.selectedJob.analysis_config.detectors[detectorIndex]; const esAgg = mlFunctionToESAggregation(detector.function); if ($scope.modelPlotEnabled === false && (esAgg === 'sum' || esAgg === 'count')) { aggType = { avg: 'sum', max: 'sum', min: 'sum' }; } mlForecastService.getForecastData( $scope.selectedJob, detectorIndex, forecastId, nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), $scope.focusAggregationInterval.expression, aggType) .then((resp) => { refreshFocusData.focusForecastData = processForecastResults(resp.results); refreshFocusData.showForecastCheckbox = (refreshFocusData.focusForecastData.length > 0); finish(); }).catch((resp) => { console.log(`Time series explorer - error loading data for forecast ID ${forecastId}`, resp); }); } // Load the data for the anomalies table. loadAnomaliesTableData(searchBounds.min.valueOf(), searchBounds.max.valueOf()); };
$scope.refresh = function () { if ($scope.selectedJob === undefined) { return; } $scope.loading = true; $scope.hasResults = false; delete $scope.chartDetails; delete $scope.contextChartData; delete $scope.focusChartData; delete $scope.contextForecastData; delete $scope.focusForecastData; // Counter to keep track of what data sets have been loaded. $scope.loadCounter++; let awaitingCount = 3; // finish() function, called after each data set has been loaded and processed. // The last one to call it will trigger the page render. function finish(counterVar) { awaitingCount--; if (awaitingCount === 0 && (counterVar === $scope.loadCounter)) { if (($scope.contextChartData && $scope.contextChartData.length) || ($scope.contextForecastData && $scope.contextForecastData.length)) { $scope.hasResults = true; } else { $scope.hasResults = false; } $scope.loading = false; // Set zoomFrom/zoomTo attributes in scope which will result in the metric chart automatically // selecting the specified range in the context chart, and so loading that date range in the focus chart. if ($scope.contextChartData.length) { const focusRange = calculateInitialFocusRange(); $scope.zoomFrom = focusRange[0]; $scope.zoomTo = focusRange[1]; } // Tell the results container directives to render. // Need to use $timeout to ensure the broadcast happens after the child scope is updated with the new data. if (($scope.contextChartData && $scope.contextChartData.length) || ($scope.contextForecastData && $scope.contextForecastData.length)) { $timeout(() => { $scope.$broadcast('render'); }, 0); } } } const bounds = timefilter.getActiveBounds(); const detectorIndex = +$scope.detectorId; $scope.modelPlotEnabled = isModelPlotEnabled($scope.selectedJob, detectorIndex, $scope.entities); // Only filter on the entity if the field has a value. const nonBlankEntities = _.filter($scope.entities, (entity) => { return entity.fieldValue.length > 0; }); $scope.criteriaFields = [{ 'fieldName': 'detector_index', 'fieldValue': detectorIndex } ].concat(nonBlankEntities); // Calculate the aggregation interval for the context chart. // Context chart swimlane will display bucket anomaly score at the same interval. $scope.contextAggregationInterval = calculateAggregationInterval(bounds, CHARTS_POINT_TARGET, CHARTS_POINT_TARGET); console.log('aggregationInterval for context data (s):', $scope.contextAggregationInterval.asSeconds()); // Ensure the search bounds align to the bucketing interval so that the first and last buckets are complete. // For sum or count detectors, short buckets would hold smaller values, and model bounds would also be affected // to some extent with all detector functions if not searching complete buckets. const searchBounds = getBoundsRoundedToInterval(bounds, $scope.contextAggregationInterval, false); // Query 1 - load metric data at low granularity across full time range. // Pass a counter flag into the finish() function to make sure we only process the results // for the most recent call to the load the data in cases where the job selection and time filter // have been altered in quick succession (such as from the job picker with 'Apply time range'). const counter = $scope.loadCounter; mlTimeSeriesSearchService.getMetricData( $scope.selectedJob, detectorIndex, nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), $scope.contextAggregationInterval.expression ).then((resp) => { const fullRangeChartData = processMetricPlotResults(resp.results, $scope.modelPlotEnabled); $scope.contextChartData = fullRangeChartData; console.log('Time series explorer context chart data set:', $scope.contextChartData); finish(counter); }).catch((resp) => { console.log('Time series explorer - error getting metric data from elasticsearch:', resp); }); // Query 2 - load max record score at same granularity as context chart // across full time range for use in the swimlane. mlResultsService.getRecordMaxScoreByTime( $scope.selectedJob.job_id, $scope.criteriaFields, searchBounds.min.valueOf(), searchBounds.max.valueOf(), $scope.contextAggregationInterval.expression ).then((resp) => { const fullRangeRecordScoreData = processRecordScoreResults(resp.results); $scope.swimlaneData = fullRangeRecordScoreData; console.log('Time series explorer swimlane anomalies data set:', $scope.swimlaneData); finish(counter); }).catch((resp) => { console.log('Time series explorer - error getting bucket anomaly scores from elasticsearch:', resp); }); // Query 3 - load details on the chart used in the chart title (charting function and entity(s)). mlTimeSeriesSearchService.getChartDetails( $scope.selectedJob, detectorIndex, $scope.entities, searchBounds.min.valueOf(), searchBounds.max.valueOf() ).then((resp) => { $scope.chartDetails = resp.results; finish(counter); }).catch((resp) => { console.log('Time series explorer - error getting entity counts from elasticsearch:', resp); }); // Plus query for forecast data if there is a forecastId stored in the appState. const forecastId = _.get($scope, 'appState.mlTimeSeriesExplorer.forecastId'); if (forecastId !== undefined) { awaitingCount++; let aggType = undefined; const detector = $scope.selectedJob.analysis_config.detectors[detectorIndex]; const esAgg = mlFunctionToESAggregation(detector.function); if ($scope.modelPlotEnabled === false && (esAgg === 'sum' || esAgg === 'count')) { aggType = { avg: 'sum', max: 'sum', min: 'sum' }; } mlForecastService.getForecastData( $scope.selectedJob, detectorIndex, forecastId, nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), $scope.contextAggregationInterval.expression, aggType) .then((resp) => { $scope.contextForecastData = processForecastResults(resp.results); finish(counter); }).catch((resp) => { console.log(`Time series explorer - error loading data for forecast ID ${forecastId}`, resp); }); } loadEntityValues(); };
function getMetricData(job, detectorIndex, entityFields, earliestMs, latestMs, interval) { if (isModelPlotEnabled(job, detectorIndex, entityFields)) { // Extract the partition, by, over fields on which to filter. const criteriaFields = []; const detector = job.analysis_config.detectors[detectorIndex]; if (_.has(detector, 'partition_field_name')) { const partitionEntity = _.find(entityFields, { 'fieldName': detector.partition_field_name }); if (partitionEntity !== undefined) { criteriaFields.push( { fieldName: 'partition_field_name', fieldValue: partitionEntity.fieldName }, { fieldName: 'partition_field_value', fieldValue: partitionEntity.fieldValue }); } } if (_.has(detector, 'over_field_name')) { const overEntity = _.find(entityFields, { 'fieldName': detector.over_field_name }); if (overEntity !== undefined) { criteriaFields.push( { fieldName: 'over_field_name', fieldValue: overEntity.fieldName }, { fieldName: 'over_field_value', fieldValue: overEntity.fieldValue }); } } if (_.has(detector, 'by_field_name')) { const byEntity = _.find(entityFields, { 'fieldName': detector.by_field_name }); if (byEntity !== undefined) { criteriaFields.push( { fieldName: 'by_field_name', fieldValue: byEntity.fieldName }, { fieldName: 'by_field_value', fieldValue: byEntity.fieldValue }); } } return mlResultsService.getModelPlotOutput( job.job_id, detectorIndex, criteriaFields, earliestMs, latestMs, interval ); } else { return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; const chartConfig = buildConfigFromDetector(job, detectorIndex); mlResultsService.getMetricData( chartConfig.datafeedConfig.indices, entityFields, chartConfig.datafeedConfig.query, chartConfig.metricFunction, chartConfig.metricFieldName, chartConfig.timeField, earliestMs, latestMs, interval ) .then((resp) => { _.each(resp.results, (value, time) => { obj.results[time] = { 'actual': value }; }); resolve(obj); }) .catch((resp) => { reject(resp); }); }); } }
return new Promise((resolve) => { this.skipCellClicks = true; // check if we can just return existing cached data if (_.isEqual(compareArgs, this.loadViewBySwimlanePreviousArgs)) { this.skipCellClicks = false; resolve({ viewBySwimlaneData: this.loadViewBySwimlanePreviousData, viewBySwimlaneDataLoading: false }); return; } this.setState({ viewBySwimlaneData: getDefaultViewBySwimlaneData(), viewBySwimlaneDataLoading: true }); const finish = (resp) => { this.skipCellClicks = false; if (resp !== undefined) { const viewBySwimlaneData = processViewByResults( resp.results, fieldValues, overallSwimlaneData, swimlaneViewByFieldName, this.getSwimlaneBucketInterval(selectedJobs).asSeconds(), ); this.loadViewBySwimlanePreviousArgs = compareArgs; this.loadViewBySwimlanePreviousData = viewBySwimlaneData; console.log('Explorer view by swimlane data set:', viewBySwimlaneData); resolve({ viewBySwimlaneData, viewBySwimlaneDataLoading: false }); } else { resolve({ viewBySwimlaneDataLoading: false }); } }; if ( selectedJobs === undefined || swimlaneViewByFieldName === undefined ) { finish(); return; } else { // Ensure the search bounds align to the bucketing interval used in the swimlane so // that the first and last buckets are complete. const bounds = timefilter.getActiveBounds(); const searchBounds = getBoundsRoundedToInterval( bounds, this.getSwimlaneBucketInterval(selectedJobs), false, ); const selectedJobIds = selectedJobs.map(d => d.id); // load scores by influencer/jobId value and time. // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets // which wouldn't be the case if e.g. '1M' was used. const interval = `${this.getSwimlaneBucketInterval(selectedJobs).asSeconds()}s`; if (swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { mlResultsService.getInfluencerValueMaxScoreByTime( selectedJobIds, swimlaneViewByFieldName, fieldValues, searchBounds.min.valueOf(), searchBounds.max.valueOf(), interval, swimlaneLimit, influencersFilterQuery ).then(finish); } else { const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; mlResultsService.getScoresByBucket( jobIds, searchBounds.min.valueOf(), searchBounds.max.valueOf(), interval, swimlaneLimit ).then(finish); } } });
return new Promise((resolve) => { // Loads the overall data components i.e. the overall swimlane and influencers list. if (selectedJobs === null) { resolve({ loading: false, hasResuts: false }); return; } // check if we can just return existing cached data const compareArgs = { selectedJobs, intervalAsSeconds: interval.asSeconds(), boundsMin: bounds.min.valueOf(), boundsMax: bounds.max.valueOf(), }; if (_.isEqual(compareArgs, this.loadOverallDataPreviousArgs)) { const overallSwimlaneData = this.loadOverallDataPreviousData; const hasResults = (overallSwimlaneData.points && overallSwimlaneData.points.length > 0); resolve({ hasResults, loading: false, overallSwimlaneData, }); return; } if (showLoadingIndicator) { this.setState({ hasResults: false, loading: true }); } // Ensure the search bounds align to the bucketing interval used in the swimlane so // that the first and last buckets are complete. const searchBounds = getBoundsRoundedToInterval( bounds, interval, false ); const selectedJobIds = selectedJobs.map(d => d.id); // Load the overall bucket scores by time. // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets // which wouldn't be the case if e.g. '1M' was used. // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works // to ensure the search is inclusive of end time. const overallBucketsBounds = getBoundsRoundedToInterval( bounds, interval, true ); mlResultsService.getOverallBucketScores( selectedJobIds, // Note there is an optimization for when top_n == 1. // If top_n > 1, we should test what happens when the request takes long // and refactor the loading calls, if necessary, to avoid delays in loading other components. 1, overallBucketsBounds.min.valueOf(), overallBucketsBounds.max.valueOf(), interval.asSeconds() + 's' ).then((resp) => { this.skipCellClicks = false; const overallSwimlaneData = processOverallResults( resp.results, searchBounds, interval.asSeconds(), ); this.loadOverallDataPreviousArgs = compareArgs; this.loadOverallDataPreviousData = overallSwimlaneData; console.log('Explorer overall swimlane data set:', overallSwimlaneData); const hasResults = (overallSwimlaneData.points && overallSwimlaneData.points.length > 0); resolve({ hasResults, loading: false, overallSwimlaneData, }); }); });
function loadViewBySwimlane(fieldValues) { // reset the swimlane data to avoid flickering where the old dataset would briefly show up. $scope.viewBySwimlaneData = getDefaultViewBySwimlaneData(); $scope.viewBySwimlaneDataLoading = true; skipCellClicks = true; // finish() function, called after each data set has been loaded and processed. // The last one to call it will trigger the page render. function finish(resp) { if (resp !== undefined) { $scope.viewBySwimlaneData = processViewByResults(resp.results, fieldValues); // do a sanity check against cellData. It can happen that a previously // selected lane loaded via URL/AppState is not available anymore. if ( $scope.cellData !== undefined && $scope.cellData.type === SWIMLANE_TYPE.VIEW_BY ) { const selectionExists = $scope.cellData.lanes.some((lane) => { return ($scope.viewBySwimlaneData.laneLabels.includes(lane)); }); if (selectionExists === false) { clearSelectedAnomalies(); } } } $scope.viewBySwimlaneDataLoading = false; skipCellClicks = false; console.log('Explorer view by swimlane data set:', $scope.viewBySwimlaneData); if (swimlaneCellClickQueue.length > 0) { const cellData = swimlaneCellClickQueue.pop(); swimlaneCellClickQueue.length = 0; $scope.swimlaneCellClick(cellData); return; } setShowViewBySwimlane(); } if ( $scope.selectedJobs === undefined || $scope.swimlaneViewByFieldName === undefined || $scope.swimlaneViewByFieldName === null ) { finish(); return; } else { // Ensure the search bounds align to the bucketing interval used in the swimlane so // that the first and last buckets are complete. const bounds = timefilter.getActiveBounds(); const searchBounds = getBoundsRoundedToInterval(bounds, $scope.swimlaneBucketInterval, false); const selectedJobIds = $scope.getSelectedJobIds(); const limit = mlSelectLimitService.state.get('limit'); const swimlaneLimit = (limit === undefined) ? SWIMLANE_DEFAULT_LIMIT : limit.val; // load scores by influencer/jobId value and time. // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets // which wouldn't be the case if e.g. '1M' was used. const interval = $scope.swimlaneBucketInterval.asSeconds() + 's'; if ($scope.swimlaneViewByFieldName !== VIEW_BY_JOB_LABEL) { mlResultsService.getInfluencerValueMaxScoreByTime( selectedJobIds, $scope.swimlaneViewByFieldName, fieldValues, searchBounds.min.valueOf(), searchBounds.max.valueOf(), interval, swimlaneLimit ).then(finish); } else { const jobIds = (fieldValues !== undefined && fieldValues.length > 0) ? fieldValues : selectedJobIds; mlResultsService.getScoresByBucket( jobIds, searchBounds.min.valueOf(), searchBounds.max.valueOf(), interval, swimlaneLimit ).then(finish); } } }