import { euiPaletteColorBlind } from '@elastic/eui' ; import { first , flatten , groupBy , isEmpty , sortBy , uniq } from 'lodash' ; import { APIReturnType } from '../../../../../../../services/rest/createCallApmApi' ; import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error' ; import { Span } from '../../../../../../../../typings/es_schemas/ui/span' ; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction' ; type TraceAPIResponse = APIReturnType < 'GET /internal/apm/traces/{traceId}' > ; interface IWaterfallGroup { [ key ] : IWaterfallSpanOrTransaction [ ] ; } const ROOT_ID = 'root' ; export enum WaterfallLegendType { ServiceName = 'serviceName' , SpanType = 'spanType' } export interface IWaterfall { entryWaterfallTransaction ? : IWaterfallTransaction ; rootTransaction ? : Transaction ; /**
     * Latency in us
     */ duration : number ; items : IWaterfallItem [ ] ; childrenByParentId : Record < string | number , IWaterfallSpanOrTransaction [ ] > ; getErrorCount : ( parentId : string ) => number ; legends : IWaterfallLegend [ ] ; errorItems : IWaterfallError [ ] ; apiResponse : TraceAPIResponse ; } interface IWaterfallSpanItemBase < TDocument , TDoctype > extends IWaterfallItemBase < TDocument , TDoctype > { /**
     * Latency in us
     */ duration : number ; legendValues : Record < WaterfallLegendType , string > ; } interface IWaterfallItemBase < TDocument , TDoctype > { doc : TDocument ; docType : TDoctype ; id : string ; parent ? : IWaterfallSpanOrTransaction ; parentId ? : string ; color : string ; /**
     * offset from first item in us
     */ offset : number ; /**
     * skew from timestamp in us
     */ skew : number ; } export type IWaterfallError = IWaterfallItemBase < APMError , 'error' > ; export type IWaterfallTransaction = IWaterfallSpanItemBase < Transaction , 'transaction' > ; export type IWaterfallSpan = IWaterfallSpanItemBase < Span , 'span' > ; export type IWaterfallSpanOrTransaction = IWaterfallTransaction | IWaterfallSpan ; export type IWaterfallItem = IWaterfallSpanOrTransaction ; export interface IWaterfallLegend { type : WaterfallLegendType ; value : string | undefined ; color : string ; } function getLegendValues ( transactionOrSpan ) { return { [ WaterfallLegendType . ServiceName ] : transactionOrSpan . service . name , [ WaterfallLegendType . SpanType ] : 'span' in transactionOrSpan ? transactionOrSpan . span . subtype || transactionOrSpan . span . type : '' , } ; } function getTransactionItem ( transaction ) { return { docType : 'transaction' , doc : transaction , id : transaction . transaction . id , parentId : transaction . parent ?. id , duration : transaction . transaction . duration . us , offset : 0 , skew : 0 , legendValues : getLegendValues ( transaction ) , color : '' , } ; } function getSpanItem ( span ) { return { docType : 'span' , doc : span , id : span . span . id , parentId : span . parent ?. id , duration : span . span . duration . us , offset : 0 , skew : 0 , legendValues : getLegendValues ( span ) , color : '' , } ; } function getErrorItem ( error , items , entryWaterfallTransaction ? ) { const entryTimestamp = entryWaterfallTransaction ?. doc . timestamp . us ?? 0 ; const parent = items . find ( ( waterfallItem ) => waterfallItem . id === error . parent ?. id ) as IWaterfallSpanOrTransaction | undefined ; const errorItem = { docType : 'error' , doc : error , id : error . error . id , parent , parentId : parent ?. id , offset : error . timestamp . us - entryTimestamp , skew : 0 , color : '' , } ; return { ... errorItem , skew : getClockSkew ( errorItem , parent ) , } ; } export function getClockSkew ( item , parentItem ? ) { if ( ! parentItem ) { return 0 ; } switch ( item . docType ) { case 'error' : case 'span' : return parentItem . skew ; case 'transaction' : { const parentStart = parentItem . doc . timestamp . us + parentItem . skew ; const offsetStart = parentStart - item . doc . timestamp . us ; if ( offsetStart > 0 ) { const latency = Math . max ( parentItem . duration - item . duration , 0 ) / 2 ; return offsetStart + latency ; } return 0 ; } } } export function getOrderedWaterfallItems ( childrenByParentId , entryWaterfallTransaction ? ) { if ( ! entryWaterfallTransaction ) { return [ ] ; } const entryTimestamp = entryWaterfallTransaction . doc . timestamp . us ; const visitedWaterfallItemSet = new Set ( ) ; function getSortedChildren ( item , parentItem ? ) { if ( visitedWaterfallItemSet . has ( item ) ) { return [ ] ; } visitedWaterfallItemSet . add ( item ) ; const children = sortBy ( childrenByParentId [ item . id ] || [ ] , 'doc.timestamp.us' ) ; item . parent = parentItem ; item . offset = item . doc . timestamp . us - entryTimestamp ; item . skew = getClockSkew ( item , parentItem ) ; const deepChildren = flatten ( children . map ( ( child ) => getSortedChildren ( child , item ) ) ) ; return [ item , ... deepChildren ] ; } return getSortedChildren ( entryWaterfallTransaction ) ; } function getRootTransaction ( childrenByParentId ) { const item = first ( childrenByParentId . root ) ; if ( item && item . docType === 'transaction' ) { return item . doc ; } } function getLegends ( waterfallItems ) { const onlyBaseSpanItems = waterfallItems . filter ( ( item ) => item . docType === 'span' || item . docType === 'transaction' ) as IWaterfallSpanOrTransaction [ ] ; const legends = [ WaterfallLegendType . ServiceName , WaterfallLegendType . SpanType , ] . flatMap ( ( legendType ) => { const allLegendValues = uniq ( onlyBaseSpanItems . map ( ( item ) => item . legendValues [ legendType ] ) ) ; const palette = euiPaletteColorBlind ( { rotations : Math . ceil ( allLegendValues . length / 10 ) , } ) ; return allLegendValues . map ( ( value , index ) => ( { type : legendType , value , color : palette [ index ] , } ) ) ; } ) ; return legends ; } const getWaterfallDuration = ( waterfallItems ) => Math . max ( ... waterfallItems . map ( ( item ) => item . offset + item . skew + ( 'duration' in item ? item . duration : 0 ) ) , 0 ) ; const getWaterfallItems = ( items ) => items . map ( ( item ) => { const docType = item . processor . event ; switch ( docType ) { case 'span' : return getSpanItem ( item as Span ) ; case 'transaction' : return getTransactionItem ( item as Transaction ) ; } } ) ; function reparentSpans ( waterfallItems ) { const childIdToParentIdMapping = Object . fromEntries ( flatten ( waterfallItems . map ( ( waterfallItem ) => { if ( waterfallItem . docType === 'span' ) { const childIds = waterfallItem . doc . child ?. id ?? [ ] ; return childIds . map ( ( id ) => [ id , waterfallItem . id ] ) ; } return [ ] ; } ) ) ) ; return waterfallItems . map ( ( waterfallItem ) => { const newParentId = childIdToParentIdMapping [ waterfallItem . id ] ; if ( newParentId ) { return { ... waterfallItem , parentId : newParentId , } ; } return waterfallItem ; } ) ; } const getChildrenGroupedByParentId = ( waterfallItems ) => groupBy ( waterfallItems , ( item ) => ( item . parentId ? item . parentId : ROOT_ID ) ) ; const getEntryWaterfallTransaction = ( entryTransactionId , waterfallItems ) : IWaterfallTransaction | undefined => waterfallItems . find ( ( item ) => item . docType === 'transaction' && item . id === entryTransactionId ) as IWaterfallTransaction ; function isInEntryTransaction ( parentIdLookup , entryTransactionId , currentId ) { if ( currentId === entryTransactionId ) { return true ; } const parentId = parentIdLookup . get ( currentId ) ; if ( parentId ) { return isInEntryTransaction ( parentIdLookup , entryTransactionId , parentId ) ; } return false ; } function getWaterfallErrors ( errorDocs , items , entryWaterfallTransaction ? ) { const errorItems = errorDocs . map ( ( errorDoc ) => getErrorItem ( errorDoc , items , entryWaterfallTransaction ) ) ; if ( ! entryWaterfallTransaction ) { return errorItems ; } const parentIdLookup = [ ... items , ... errorItems ] . reduce ( ( map , { id , parentId } ) => { map . set ( id , parentId ?? ROOT_ID ) ; return map ; } , new Map < string , string > ( ) ) ; return errorItems . filter ( ( errorItem ) => isInEntryTransaction ( parentIdLookup , entryWaterfallTransaction ?. id , errorItem . id ) ) ; } function getErrorCountByParentId ( errorDocs ) { return errorDocs . reduce < Record < string , number > > ( ( acc , doc ) => { const parentId = doc . parent ?. id ; if ( ! parentId ) { return acc ; } acc [ parentId ] = ( acc [ parentId ] ?? 0 ) + 1 ; return acc ; } , { } ) ; } export function getWaterfall ( apiResponse , entryTransactionId ? ) { if ( isEmpty ( apiResponse . traceDocs ) || ! entryTransactionId ) { return { apiResponse , duration : 0 , items : [ ] , legends : [ ] , errorItems : [ ] , childrenByParentId : { } , getErrorCount : ( ) => 0 , } ; } const errorCountByParentId = getErrorCountByParentId ( apiResponse . errorDocs ) ; const waterfallItems = getWaterfallItems ( apiResponse . traceDocs ) ; const childrenByParentId = getChildrenGroupedByParentId ( reparentSpans ( waterfallItems ) ) ; const entryWaterfallTransaction = getEntryWaterfallTransaction ( entryTransactionId , waterfallItems ) ; const items = getOrderedWaterfallItems ( childrenByParentId , entryWaterfallTransaction ) ; const errorItems = getWaterfallErrors ( apiResponse . errorDocs , items , entryWaterfallTransaction ) ; const rootTransaction = getRootTransaction ( childrenByParentId ) ; const duration = getWaterfallDuration ( items ) ; const legends = getLegends ( items ) ; return { apiResponse , entryWaterfallTransaction , rootTransaction , duration , items , legends , errorItems , childrenByParentId : getChildrenGroupedByParentId ( items ) , getErrorCount : ( parentId ) => errorCountByParentId [ parentId ] ?? 0 , } ; }