import { IUiSettingsClient } from 'kibana/public' ; import { TimefilterContract , TimeRange , UI_SETTINGS , } from '../../../../../../src/plugins/data/public' ; import { getBoundsRoundedToInterval , TimeBuckets , TimeBucketsInterval , TimeRangeBounds , } from '../util/time_buckets' ; import { ExplorerJob , OverallSwimlaneData , SwimlaneData , ViewBySwimLaneData , } from '../explorer/explorer_utils' ; import { OVERALL_LABEL , VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants' ; import { MlResultsService } from './results_service' ; import { EntityField } from '../../../common/util/anomaly_utils' ; import { InfluencersFilterQuery } from '../../../common/types/es_client' ; import { isPopulatedObject } from '../../../common' ; /**
 * Service for retrieving anomaly swim lanes data.
 */ export class AnomalyTimelineService { private timeBuckets : TimeBuckets ; private _customTimeRange : TimeRange | undefined ; constructor ( private timeFilter , uiSettings , private mlResultsService ) { this . timeBuckets = new TimeBuckets ( { 'histogram:maxBars' : uiSettings . get ( UI_SETTINGS . HISTOGRAM_MAX_BARS ) , 'histogram:barTarget' : uiSettings . get ( UI_SETTINGS . HISTOGRAM_BAR_TARGET ) , dateFormat : uiSettings . get ( 'dateFormat' ) , 'dateFormat:scaled' : uiSettings . get ( 'dateFormat:scaled' ) , } ) ; this . timeFilter . enableTimeRangeSelector ( ) ; } public static isSwimlaneData ( arg ) { return isPopulatedObject ( arg , [ 'interval' , 'points' , 'laneLabels' ] ) ; } public static isOverallSwimlaneData ( arg ) { return ( this . isSwimlaneData ( arg ) && isPopulatedObject ( arg , [ 'earliest' , 'latest' ] ) && arg . laneLabels . length === 1 && arg . laneLabels [ 0 ] === OVERALL_LABEL ) ; } public setTimeRange ( timeRange ) { this . _customTimeRange = timeRange ; } public getSwimlaneBucketInterval ( selectedJobs , swimlaneContainerWidth ) { const bounds = this . getTimeBounds ( ) ; if ( bounds === undefined ) { throw new Error ( 'timeRangeSelectorEnabled has to be enabled' ) ; } this . timeBuckets . setInterval ( 'auto' ) ; this . timeBuckets . setBounds ( bounds ) ; const intervalSeconds = this . timeBuckets . getInterval ( ) . asSeconds ( ) ; const timerangeSeconds = ( bounds . max ! . valueOf ( ) - bounds . min ! . valueOf ( ) ) / 1000 ; const numBuckets = timerangeSeconds / intervalSeconds ; const cellWidth = Math . floor ( ( swimlaneContainerWidth / numBuckets ) * 100 ) / 100 ; if ( cellWidth < 8 ) { this . timeBuckets . setInterval ( intervalSeconds * 2 + 's' ) ; } const maxBucketSpanSeconds = selectedJobs . reduce ( ( memo , job ) => Math . max ( memo , job . bucketSpanSeconds ) , 0 ) ; if ( maxBucketSpanSeconds > intervalSeconds ) { this . timeBuckets . setInterval ( maxBucketSpanSeconds + 's' ) ; this . timeBuckets . setBounds ( bounds ) ; } return this . timeBuckets . getInterval ( ) ; } param selectedJobs param chartWidth public async loadOverallData ( selectedJobs , chartWidth ? , bucketInterval ? , overallScore ? ) { const interval = bucketInterval ?? this . getSwimlaneBucketInterval ( selectedJobs , chartWidth ! ) ; if ( ! selectedJobs || ! selectedJobs . length ) { throw new Error ( 'Explorer jobs collection is required' ) ; } const bounds = this . getTimeBounds ( ) ; const searchBounds = getBoundsRoundedToInterval ( bounds , interval , false ) ; const selectedJobIds = selectedJobs . map ( ( d ) => d . id ) ; const overallBucketsBounds = getBoundsRoundedToInterval ( bounds , interval , true ) ; const resp = await this . mlResultsService . getOverallBucketScores ( selectedJobIds , 1 , overallBucketsBounds . min . valueOf ( ) , overallBucketsBounds . max . valueOf ( ) , interval . asSeconds ( ) + 's' , overallScore ) ; const overallSwimlaneData = this . processOverallResults ( resp . results , searchBounds , interval . asSeconds ( ) ) ; return overallSwimlaneData ; } param fieldValues param bounds param selectedJobs param viewBySwimlaneFieldName param swimlaneLimit param perPage param fromPage param swimlaneContainerWidth param influencersFilterQuery public async loadViewBySwimlane ( fieldValues , bounds , selectedJobs , viewBySwimlaneFieldName , swimlaneLimit , perPage , fromPage , swimlaneContainerWidth ? , influencersFilterQuery ? , bucketInterval ? , swimLaneSeverity ? ) { const timefilterBounds = this . getTimeBounds ( ) ; if ( timefilterBounds === undefined ) { throw new Error ( 'timeRangeSelectorEnabled has to be enabled' ) ; } const swimlaneBucketInterval = bucketInterval ?? this . getSwimlaneBucketInterval ( selectedJobs , swimlaneContainerWidth ! ) ; const searchBounds = getBoundsRoundedToInterval ( timefilterBounds , swimlaneBucketInterval , false ) ; const selectedJobIds = selectedJobs . map ( ( d ) => d . id ) ; const intervalMs = swimlaneBucketInterval . asMilliseconds ( ) ; let response ; if ( viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL ) { const jobIds = fieldValues !== undefined && fieldValues . length > 0 ? fieldValues : selectedJobIds ; response = await this . mlResultsService . getScoresByBucket ( jobIds , searchBounds . min . valueOf ( ) , searchBounds . max . valueOf ( ) , intervalMs , perPage , fromPage , swimLaneSeverity ) ; } else { response = await this . mlResultsService . getInfluencerValueMaxScoreByTime ( selectedJobIds , viewBySwimlaneFieldName , fieldValues , searchBounds . min . valueOf ( ) , searchBounds . max . valueOf ( ) , intervalMs , swimlaneLimit , perPage , fromPage , influencersFilterQuery , swimLaneSeverity ) ; } if ( response === undefined ) { return ; } const viewBySwimlaneData = this . processViewByResults ( response . results , response . cardinality , fieldValues , bounds , viewBySwimlaneFieldName , swimlaneBucketInterval . asSeconds ( ) ) ; return viewBySwimlaneData ; } public async loadViewByTopFieldValuesForSelectedTime ( earliestMs , latestMs , selectedJobs , viewBySwimlaneFieldName , swimlaneLimit , perPage , fromPage , swimlaneContainerWidth , selectionInfluencers , influencersFilterQuery ) { const selectedJobIds = selectedJobs . map ( ( d ) => d . id ) ; if ( viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL ) { const resp = await this . mlResultsService . getTopInfluencers ( selectedJobIds , earliestMs , latestMs , swimlaneLimit , perPage , fromPage , selectionInfluencers , influencersFilterQuery ) ; if ( resp . influencers [ viewBySwimlaneFieldName ] === undefined ) { return [ ] ; } const topFieldValues = [ ] ; const topInfluencers = resp . influencers [ viewBySwimlaneFieldName ] ; if ( Array . isArray ( topInfluencers ) ) { topInfluencers . forEach ( ( influencerData ) => { if ( influencerData . maxAnomalyScore > 0 ) { topFieldValues . push ( influencerData . influencerFieldValue ) ; } } ) ; } return topFieldValues ; } else { const resp = await this . mlResultsService . getScoresByBucket ( selectedJobIds , earliestMs , latestMs , this . getSwimlaneBucketInterval ( selectedJobs , swimlaneContainerWidth ) . asMilliseconds ( ) , perPage , fromPage , swimlaneLimit ) ; return Object . keys ( resp . results ) ; } } private getTimeBounds ( ) { return this . _customTimeRange !== undefined ? this . timeFilter . calculateBounds ( this . _customTimeRange ) : this . timeFilter . getBounds ( ) ; } private processOverallResults ( scoresByTime , searchBounds , interval ) { const overallLabel = OVERALL_LABEL ; const dataset = { laneLabels : [ overallLabel ] , points : [ ] , interval , earliest : searchBounds . min . valueOf ( ) / 1000 , latest : searchBounds . max . valueOf ( ) / 1000 , } ; Object . entries ( scoresByTime ) . forEach ( ( [ timeMs , score ] ) => { const time = + timeMs / 1000 ; dataset . points . push ( { laneLabel : overallLabel , time , value : score , } ) ; dataset . earliest = Math . min ( time , dataset . earliest ) ; dataset . latest = Math . max ( time + dataset . interval , dataset . latest ) ; } ) ; return dataset ; } private processViewByResults ( scoresByInfluencerAndTime , cardinality , sortedLaneValues , bounds , viewBySwimlaneFieldName , interval ) { const dataset = { fieldName : viewBySwimlaneFieldName , points : [ ] , laneLabels : [ ] , interval , earliest : bounds . earliest , latest : bounds . latest , cardinality , } ; const maxScoreByLaneLabel = { } ; Object . entries ( scoresByInfluencerAndTime ) . forEach ( ( [ influencerFieldValue , influencerData ] ) => { dataset . laneLabels . push ( influencerFieldValue ) ; maxScoreByLaneLabel [ influencerFieldValue ] = 0 ; Object . entries ( influencerData ) . forEach ( ( [ timeMs , anomalyScore ] ) => { const time = + timeMs / 1000 ; dataset . points . push ( { laneLabel : influencerFieldValue , time , value : anomalyScore , } ) ; maxScoreByLaneLabel [ influencerFieldValue ] = Math . max ( maxScoreByLaneLabel [ influencerFieldValue ] , anomalyScore ) ; } ) ; } ) ; const sortValuesLength = sortedLaneValues . length ; if ( sortValuesLength === 0 ) { dataset . laneLabels = dataset . laneLabels . sort ( ( a , b ) => { return maxScoreByLaneLabel [ b ] - maxScoreByLaneLabel [ a ] ; } ) ; } else { dataset . laneLabels = dataset . laneLabels . sort ( ( a , b ) => { let aIndex = sortedLaneValues . indexOf ( a ) ; let bIndex = sortedLaneValues . indexOf ( b ) ; aIndex = aIndex > - 1 ? aIndex : sortValuesLength ; bIndex = bIndex > - 1 ? bIndex : sortValuesLength ; return aIndex - bIndex ; } ) ; } return dataset ; } }