"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createJobs = createJobs; exports.escapeDoubleQuotes = escapeDoubleQuotes; exports.escapeParens = escapeParens; exports.escapeRegExp = escapeRegExp; exports.getClearedSelectedAnomaliesState = getClearedSelectedAnomaliesState; exports.getDataViewsAndIndicesWithGeoFields = getDataViewsAndIndicesWithGeoFields; exports.getDateFormatTz = getDateFormatTz; exports.getDefaultSwimlaneData = getDefaultSwimlaneData; exports.getFieldsByJob = getFieldsByJob; exports.getInfluencers = getInfluencers; exports.getQueryPattern = getQueryPattern; exports.getSelectionInfluencers = getSelectionInfluencers; exports.getSelectionJobIds = getSelectionJobIds; exports.getSelectionTimeRange = getSelectionTimeRange; exports.isExplorerJob = isExplorerJob; exports.loadAnnotationsTableData = loadAnnotationsTableData; exports.loadAnomaliesTableData = loadAnomaliesTableData; exports.loadFilteredTopInfluencers = loadFilteredTopInfluencers; exports.loadOverallAnnotations = loadOverallAnnotations; exports.loadTopInfluencers = loadTopInfluencers; exports.removeFilterFromQueryString = removeFilterFromQueryString; var _lodash = require("lodash"); var _momentTimezone = _interopRequireDefault(require("moment-timezone")); var _rxjs = require("rxjs"); var _fieldTypes = require("@kbn/field-types"); var _mlIsPopulatedObject = require("@kbn/ml-is-populated-object"); var _mlErrorUtils = require("@kbn/ml-error-utils"); var _mlAnomalyUtils = require("@kbn/ml-anomaly-utils"); var _search = require("../../../common/constants/search"); var _index_utils = require("../util/index_utils"); var _job_utils = require("../../../common/util/job_utils"); var _parse_interval = require("../../../common/util/parse_interval"); var _ml_api_service = require("../services/ml_api_service"); var _job_service = require("../services/job_service"); var _dependency_cache = require("../util/dependency_cache"); var _explorer_constants = require("./explorer_constants"); /* * 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. */ /* * utils for Anomaly Explorer. */ function isExplorerJob(arg) { return (0, _mlIsPopulatedObject.isPopulatedObject)(arg) && typeof arg.id === 'string' && arg.selected !== undefined && arg.bucketSpanSeconds !== undefined; } // create new job objects based on standard job config objects function createJobs(jobs) { return jobs.map(job => { var _job$model_plot_confi; const bucketSpan = (0, _parse_interval.parseInterval)(job.analysis_config.bucket_span); return { id: job.job_id, selected: false, bucketSpanSeconds: bucketSpan.asSeconds(), isSingleMetricViewerJob: (0, _job_utils.isTimeSeriesViewJob)(job), sourceIndices: job.datafeed_config.indices, modelPlotEnabled: ((_job$model_plot_confi = job.model_plot_config) === null || _job$model_plot_confi === void 0 ? void 0 : _job$model_plot_confi.enabled) === true }; }); } function getClearedSelectedAnomaliesState() { return { selectedCells: undefined }; } function getDefaultSwimlaneData() { return { fieldName: '', laneLabels: [], points: [], interval: 3600 }; } async function loadFilteredTopInfluencers(mlResultsService, jobIds, earliestMs, latestMs, records, influencers, noInfluencersConfigured, influencersFilterQuery) { // 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. 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] = (0, _lodash.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 }); }); } }); return await loadTopInfluencers(mlResultsService, jobIds, earliestMs, latestMs, filterInfluencers, noInfluencersConfigured, influencersFilterQuery); } function getInfluencers(selectedJobs) { const influencers = []; selectedJobs.forEach(selectedJob => { const job = _job_service.mlJobService.getJob(selectedJob.id); if (job !== undefined && job.analysis_config && job.analysis_config.influencers) { influencers.push(...job.analysis_config.influencers); } }); return influencers; } function getDateFormatTz() { const uiSettings = (0, _dependency_cache.getUiSettings)(); // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. const tzConfig = uiSettings.get('dateFormat:tz'); const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : _momentTimezone.default.tz.guess(); return dateFormatTz; } function getFieldsByJob() { return _job_service.mlJobService.jobs.reduce((reducedFieldsByJob, job) => { // Add the list of distinct by, over, partition and influencer fields for each job. const analysisConfig = job.analysis_config; const influencers = analysisConfig.influencers || []; const fieldsForJob = (analysisConfig.detectors || []).reduce((reducedfieldsForJob, detector) => { if (detector.partition_field_name !== undefined) { reducedfieldsForJob.push(detector.partition_field_name); } if (detector.over_field_name !== undefined) { reducedfieldsForJob.push(detector.over_field_name); } // For jobs with by and over fields, don't add the 'by' field as this // field will only be added to the top-level fields for record type results // if it also an influencer over the bucket. if (detector.by_field_name !== undefined && detector.over_field_name === undefined) { reducedfieldsForJob.push(detector.by_field_name); } return reducedfieldsForJob; }, []).concat(influencers); reducedFieldsByJob[job.job_id] = (0, _lodash.uniq)(fieldsForJob); reducedFieldsByJob['*'] = (0, _lodash.union)(reducedFieldsByJob['*'], reducedFieldsByJob[job.job_id]); return reducedFieldsByJob; }, { '*': [] }); } function getSelectionTimeRange(selectedCells, bounds) { // Returns the time range of the cell(s) currently selected in the swimlane. // If no cell(s) are currently selected, returns the dashboard time range. // TODO check why this code always expect both min and max defined. const requiredBounds = bounds; let earliestMs = requiredBounds.min.valueOf(); let latestMs = requiredBounds.max.valueOf(); if ((selectedCells === null || selectedCells === void 0 ? void 0 : selectedCells.times) !== undefined) { // time property of the cell data is an array, with the elements being // the start times of the first and last cell selected. earliestMs = selectedCells.times[0] !== undefined ? selectedCells.times[0] * 1000 : requiredBounds.min.valueOf(); latestMs = requiredBounds.max.valueOf(); if (selectedCells.times[1] !== undefined) { // Subtract 1 ms so search does not include start of next bucket. latestMs = selectedCells.times[1] * 1000 - 1; } } return { earliestMs, latestMs }; } function getSelectionInfluencers(selectedCells, fieldName) { if (!!selectedCells && selectedCells.type !== _explorer_constants.SWIMLANE_TYPE.OVERALL && selectedCells.viewByFieldName !== undefined && selectedCells.viewByFieldName !== _explorer_constants.VIEW_BY_JOB_LABEL) { return selectedCells.lanes.map(laneLabel => ({ fieldName, fieldValue: laneLabel })); } return []; } function getSelectionJobIds(selectedCells, selectedJobs) { if (!!selectedCells && selectedCells.type !== _explorer_constants.SWIMLANE_TYPE.OVERALL && selectedCells.viewByFieldName !== undefined && selectedCells.viewByFieldName === _explorer_constants.VIEW_BY_JOB_LABEL) { return selectedCells.lanes; } return selectedJobs.map(d => d.id); } function loadOverallAnnotations(selectedJobs, bounds) { const jobIds = selectedJobs.map(d => d.id); const timeRange = getSelectionTimeRange(undefined, bounds); return new Promise(resolve => { (0, _rxjs.lastValueFrom)(_ml_api_service.ml.annotations.getAnnotations$({ jobIds, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, maxAnnotations: _search.ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE })).then(resp => { if (resp.error !== undefined || resp.annotations === undefined) { const errorMessage = (0, _mlErrorUtils.extractErrorMessage)(resp.error); return resolve({ annotationsData: [], error: errorMessage !== '' ? errorMessage : undefined }); } const annotationsData = []; jobIds.forEach(jobId => { const jobAnnotations = resp.annotations[jobId]; if (jobAnnotations !== undefined) { annotationsData.push(...jobAnnotations); } }); return resolve({ annotationsData: annotationsData.sort((a, b) => { return a.timestamp - b.timestamp; }).map((d, i) => { d.key = (i + 1).toString(); return d; }) }); }).catch(resp => { const errorMessage = (0, _mlErrorUtils.extractErrorMessage)(resp); return resolve({ annotationsData: [], error: errorMessage !== '' ? errorMessage : undefined }); }); }); } function loadAnnotationsTableData(selectedCells, selectedJobs, bounds) { const jobIds = getSelectionJobIds(selectedCells, selectedJobs); const timeRange = getSelectionTimeRange(selectedCells, bounds); return new Promise(resolve => { (0, _rxjs.lastValueFrom)(_ml_api_service.ml.annotations.getAnnotations$({ jobIds, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, maxAnnotations: _search.ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE })).then(resp => { if (resp.error !== undefined || resp.annotations === undefined) { const errorMessage = (0, _mlErrorUtils.extractErrorMessage)(resp.error); return resolve({ annotationsData: [], totalCount: 0, error: errorMessage !== '' ? errorMessage : undefined }); } const annotationsData = []; jobIds.forEach(jobId => { const jobAnnotations = resp.annotations[jobId]; if (jobAnnotations !== undefined) { annotationsData.push(...jobAnnotations); } }); return resolve({ annotationsData: annotationsData.sort((a, b) => { return a.timestamp - b.timestamp; }).map((d, i) => { d.key = (i + 1).toString(); return d; }), totalCount: resp.totalCount }); }).catch(resp => { const errorMessage = (0, _mlErrorUtils.extractErrorMessage)(resp); return resolve({ annotationsData: [], totalCount: 0, error: errorMessage !== '' ? errorMessage : undefined }); }); }); } async function loadAnomaliesTableData(selectedCells, selectedJobs, dateFormatTz, bounds, fieldName, tableInterval, tableSeverity, influencersFilterQuery) { const jobIds = getSelectionJobIds(selectedCells, selectedJobs); const influencers = getSelectionInfluencers(selectedCells, fieldName); const timeRange = getSelectionTimeRange(selectedCells, bounds); return new Promise((resolve, reject) => { _ml_api_service.ml.results.getAnomaliesTableData(jobIds, [], influencers, tableInterval, tableSeverity, timeRange.earliestMs, timeRange.latestMs, dateFormatTz, _search.ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, _explorer_constants.MAX_CATEGORY_EXAMPLES, influencersFilterQuery).toPromise().then(resp => { const anomalies = resp.anomalies; const detectorsByJob = _job_service.mlJobService.detectorsByJob; // @ts-ignore anomalies.forEach(anomaly => { // Add a detector property to each anomaly. // Default to functionDescription if no description available. // TODO - when job_service is moved server_side, move this to server endpoint. const jobId = anomaly.jobId; const detector = (0, _lodash.get)(detectorsByJob, [jobId, anomaly.detectorIndex]); anomaly.detector = (0, _lodash.get)(detector, ['detector_description'], anomaly.source.function_description); // For detectors with rules, add a property with the rule count. if (detector !== undefined && detector.custom_rules !== undefined) { anomaly.rulesLength = detector.custom_rules.length; } // Add properties used for building the links menu. // TODO - when job_service is moved server_side, move this to server endpoint. const job = _job_service.mlJobService.getJob(jobId); let isChartable = (0, _job_utils.isSourceDataChartableForDetector)(job, anomaly.detectorIndex); if (isChartable === false && (0, _job_utils.isModelPlotChartableForDetector)(job, anomaly.detectorIndex)) { // Check if model plot is enabled for this job. // Need to check the entity fields for the record in case the model plot config has a terms list. // If terms is specified, model plot is only stored if both the partition and by fields appear in the list. const entityFields = (0, _mlAnomalyUtils.getEntityFieldList)(anomaly.source); isChartable = (0, _job_utils.isModelPlotEnabled)(job, anomaly.detectorIndex, entityFields); } anomaly.isTimeSeriesViewRecord = isChartable; anomaly.isGeoRecord = detector !== undefined && detector.function === _mlAnomalyUtils.ML_JOB_AGGREGATION.LAT_LONG; if (_job_service.mlJobService.customUrlsByJob[jobId] !== undefined) { anomaly.customUrls = _job_service.mlJobService.customUrlsByJob[jobId]; } }); resolve({ anomalies, interval: resp.interval, examplesByJobId: resp.examplesByJobId, showViewSeriesLink: true, jobIds }); }).catch(resp => { // eslint-disable-next-line no-console console.log('Explorer - error loading data for anomalies table:', resp); reject(); }); }); } async function loadTopInfluencers(mlResultsService, selectedJobIds, earliestMs, latestMs, influencers, noInfluencersConfigured, influencersFilterQuery) { return new Promise(resolve => { if (noInfluencersConfigured !== true) { mlResultsService.getTopInfluencers(selectedJobIds, earliestMs, latestMs, _explorer_constants.MAX_INFLUENCER_FIELD_VALUES, 10, 1, influencers, influencersFilterQuery).then(resp => { // TODO - sort the influencers keys so that the partition field(s) are first. resolve(resp.influencers); }); } else { resolve({}); } }); } // Recommended by MDN for escaping user input to be treated as a literal string within a regular expression // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } function escapeParens(string) { return string.replace(/[()]/g, '\\$&'); } function escapeDoubleQuotes(string) { return string.replace(/[\\"]/g, '\\$&'); } function getQueryPattern(fieldName, fieldValue) { const sanitizedFieldName = escapeRegExp(fieldName); const sanitizedFieldValue = escapeRegExp(fieldValue); return new RegExp(`(${sanitizedFieldName})\\s?:\\s?(")?(${sanitizedFieldValue})(")?`, 'i'); } function removeFilterFromQueryString(currentQueryString, fieldName, fieldValue) { let newQueryString = ''; // Remove the passed in fieldName and value from the existing filter const queryPattern = getQueryPattern(fieldName, fieldValue); newQueryString = currentQueryString.replace(queryPattern, ''); // match 'and' or 'or' at the start/end of the string const endPattern = /\s(and|or)\s*$/gi; const startPattern = /^\s*(and|or)\s/gi; // If string has a double operator (e.g. tag:thing or or tag:other) remove and replace with the first occurring operator const invalidOperatorPattern = /\s+(and|or)\s+(and|or)\s+/gi; newQueryString = newQueryString.replace(invalidOperatorPattern, ' $1 '); // If string starts/ends with 'and' or 'or' remove that as that is illegal kuery syntax newQueryString = newQueryString.replace(endPattern, ''); newQueryString = newQueryString.replace(startPattern, ''); return newQueryString; } // Returns an object mapping job ids to source indices which map to geo fields for that index async function getDataViewsAndIndicesWithGeoFields(selectedJobs, dataViewsService) { const sourceIndicesWithGeoFieldsMap = {}; // Avoid searching for data view again if previous job already has same source index const dataViewsMap = new Map(); // Go through selected jobs if (Array.isArray(selectedJobs)) { for (const job of selectedJobs) { let sourceIndices; let jobId; if (isExplorerJob(job)) { sourceIndices = job.sourceIndices; jobId = job.id; } else { sourceIndices = job.datafeed_config.indices; jobId = job.job_id; } if (Array.isArray(sourceIndices)) { for (const sourceIndex of sourceIndices) { var _cachedDV$id; const cachedDV = dataViewsMap.get(sourceIndex); const dataViewId = (_cachedDV$id = cachedDV === null || cachedDV === void 0 ? void 0 : cachedDV.id) !== null && _cachedDV$id !== void 0 ? _cachedDV$id : await (0, _index_utils.getDataViewIdFromName)(sourceIndex); if (dataViewId) { const dataView = cachedDV !== null && cachedDV !== void 0 ? cachedDV : await dataViewsService.get(dataViewId); if (!dataView) { continue; } dataViewsMap.set(sourceIndex, dataView); const geoFields = [...dataView.fields.getByType(_fieldTypes.ES_FIELD_TYPES.GEO_POINT), ...dataView.fields.getByType(_fieldTypes.ES_FIELD_TYPES.GEO_SHAPE)]; if (geoFields.length > 0) { if (sourceIndicesWithGeoFieldsMap[jobId] === undefined) { sourceIndicesWithGeoFieldsMap[jobId] = { [sourceIndex]: { geoFields: [], dataViewId } }; } sourceIndicesWithGeoFieldsMap[jobId][sourceIndex].geoFields.push(...geoFields.map(field => field.name)); } } } } } } return { sourceIndicesWithGeoFieldsMap, dataViews: [...dataViewsMap.values()] }; }