"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAnomalyTimeseries = getAnomalyTimeseries; var _lodash = require("lodash"); var _server = require("@kbn/observability-plugin/server"); var _common = require("@kbn/data-plugin/common"); var _apm_ml_anomaly_query = require("./apm_ml_anomaly_query"); var _apm_ml_detectors = require("../../../common/anomaly_detection/apm_ml_detectors"); var _apm_ml_jobs_query = require("./apm_ml_jobs_query"); var _as_mutable_array = require("../../../common/utils/as_mutable_array"); var _maybe = require("../../../common/utils/maybe"); var _anomaly_search = require("./anomaly_search"); var _get_anomaly_result_bucket_size = require("./get_anomaly_result_bucket_size"); var _get_ml_jobs_with_apm_group = require("./get_ml_jobs_with_apm_group"); /* * 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 FALLBACK_ML_BUCKET_SPAN = 15; // minutes function divide(value, divider) { if (value === null) { return null; } return value / divider; } // Expected bounds are retrieved with bucket span interval padded to the time range // so we need to cut the excess bounds to just the start and end time // so that the chart show up correctly without the padded time function getBoundedX(value, start, end) { if (value === null) { return null; } if (value < start) return start; if (value > end) return end; return value; } async function getAnomalyTimeseries({ serviceName, transactionType, start, end, logger, mlClient, environment: preferredEnvironment }) { var _mlJobs$find, _parseInterval$asSeco, _parseInterval, _anomaliesResponse$ag, _anomaliesResponse$ag2; if (!mlClient) { return []; } const mlJobs = await (0, _get_ml_jobs_with_apm_group.getMlJobsWithAPMGroup)(mlClient.anomalyDetectors); if (!mlJobs.length) { return []; } // If multiple ML jobs exist // find the first job with valid running datafeed that matches the preferred environment const preferredBucketSpan = (_mlJobs$find = mlJobs.find(j => j.datafeedState !== undefined && j.environment === preferredEnvironment)) === null || _mlJobs$find === void 0 ? void 0 : _mlJobs$find.bucketSpan; const minBucketSize = (_parseInterval$asSeco = (_parseInterval = (0, _common.parseInterval)(preferredBucketSpan !== null && preferredBucketSpan !== void 0 ? preferredBucketSpan : `${FALLBACK_ML_BUCKET_SPAN}m`)) === null || _parseInterval === void 0 ? void 0 : _parseInterval.asSeconds()) !== null && _parseInterval$asSeco !== void 0 ? _parseInterval$asSeco : FALLBACK_ML_BUCKET_SPAN * 60; // secs // Expected bounds (aka ML model plots) are stored as points in time, in intervals of the predefined bucket_span, // so to query bounds that include start and end time // we need to append bucket size before and after the range const extendedStart = start - minBucketSize * 1000; // ms const extendedEnd = end + minBucketSize * 1000; // ms const { intervalString } = (0, _get_anomaly_result_bucket_size.getAnomalyResultBucketSize)({ start: extendedStart, end: extendedEnd, // If the calculated bucketSize is smaller than the bucket span interval, // use the original job's bucket_span minBucketSize }); const anomaliesResponse = await (0, _anomaly_search.anomalySearch)(mlClient.mlSystem.mlAnomalySearch, { body: { size: 0, query: { bool: { filter: [...(0, _apm_ml_anomaly_query.apmMlAnomalyQuery)({ serviceName, transactionType }), ...(0, _server.rangeQuery)(extendedStart, extendedEnd, 'timestamp'), ...(0, _apm_ml_jobs_query.apmMlJobsQuery)(mlJobs)] } }, aggs: { by_timeseries_id: { composite: { size: 5000, sources: (0, _as_mutable_array.asMutableArray)([{ jobId: { terms: { field: 'job_id' } } }, { detectorIndex: { terms: { field: 'detector_index' } } }, { serviceName: { terms: { field: 'partition_field_value' } } }, { transactionType: { terms: { field: 'by_field_value' } } }]) }, aggs: { timeseries: { date_histogram: { field: 'timestamp', fixed_interval: intervalString, extended_bounds: { min: extendedStart, max: extendedEnd } }, aggs: { top_anomaly: { top_metrics: { metrics: (0, _as_mutable_array.asMutableArray)([{ field: 'record_score' }, { field: 'actual' }]), size: 1, sort: { record_score: 'desc' } } }, model_lower: { min: { field: 'model_lower' } }, model_upper: { max: { field: 'model_upper' } } } } } } } } }); const jobsById = (0, _lodash.keyBy)(mlJobs, job => job.jobId); const series = (_anomaliesResponse$ag = (_anomaliesResponse$ag2 = anomaliesResponse.aggregations) === null || _anomaliesResponse$ag2 === void 0 ? void 0 : _anomaliesResponse$ag2.by_timeseries_id.buckets.map(bucket => { const jobId = bucket.key.jobId; const job = (0, _maybe.maybe)(jobsById[jobId]); if (!job) { logger.warn(`Could not find job for id ${jobId}`); return undefined; } const type = (0, _apm_ml_detectors.getApmMlDetectorType)(Number(bucket.key.detectorIndex)); // ml failure rate is stored as 0-100, we calculate failure rate as 0-1 const divider = type === _apm_ml_detectors.ApmMlDetectorType.txFailureRate ? 100 : 1; return { jobId, type, serviceName: bucket.key.serviceName, environment: job.environment, transactionType: bucket.key.transactionType, version: job.version, anomalies: bucket.timeseries.buckets.map(dateBucket => { var _ref, _dateBucket$top_anoma, _ref2, _dateBucket$top_anoma2; return { x: dateBucket.key, y: (_ref = (_dateBucket$top_anoma = dateBucket.top_anomaly.top[0]) === null || _dateBucket$top_anoma === void 0 ? void 0 : _dateBucket$top_anoma.metrics.record_score) !== null && _ref !== void 0 ? _ref : null, actual: divide((_ref2 = (_dateBucket$top_anoma2 = dateBucket.top_anomaly.top[0]) === null || _dateBucket$top_anoma2 === void 0 ? void 0 : _dateBucket$top_anoma2.metrics.actual) !== null && _ref2 !== void 0 ? _ref2 : null, divider) }; }), bounds: bucket.timeseries.buckets.map(dateBucket => { return { x: getBoundedX(dateBucket.key, start, end), y0: divide(dateBucket.model_lower.value, divider), y1: divide(dateBucket.model_upper.value, divider) }; }) }; })) !== null && _anomaliesResponse$ag !== void 0 ? _anomaliesResponse$ag : []; return (0, _lodash.compact)(series); }