import { FabricEventBase , ClusterEvent , NodeEvent , ApplicationEvent , FabricEvent , PartitionEvent } from './Events' ; import { DataGroup , DataItem , DataSet } from 'vis-timeline/standalone/esm' ; import padStart from 'lodash/padStart' ; import findIndex from 'lodash/findIndex' ; import { HtmlUtils } from 'src/app/Utils/HtmlUtils' ; import { RepairTask } from 'src/app/Models/DataModels/repairTask' ; export interface ITimelineData { groups : DataSet < DataGroup > ; items : DataSet < DataItem > ; start ? : Date ; end ? : Date ; potentiallyMissingEvents ? : boolean ; } export interface ITimelineDataGenerator < T extends FabricEventBase > { consume ( events , startOfRange , endOfRange ) : ITimelineData ; } export class EventStoreUtils { private static internalToolTipFormatterObject = ( data ) => { const rows = Object . keys ( data ) . map ( key => EventStoreUtils . internalToolTipFormatter ( key , data [ key ] ) ) . join ( '' ) ; return `<table style="word-break: break-all;"><tbody>${ rows }</tbody></table>` ; } ; private static internalToolTipFormatter = ( key , data ) => { let value = data ; if ( Array . isArray ( data ) ) { value = data . map ( arrValue => EventStoreUtils . internalToolTipFormatter ( '' , arrValue ) ) . join ( '' ) ; } else if ( typeof data === 'object' ) { value = EventStoreUtils . internalToolTipFormatterObject ( data ) ; } return `<tr style="padding: 0 5 px; bottom-border: 1px solid gray"><td style="word-break: keep-all;">${ key }</td><td style="display:flex; flex-direction: row; "> <div style="margin-right: 4px">:</div style="white-space: pre-wrap; display: inline-block;"> ${ value }</td></tr>` ; } ; public static tooltipFormat = ( data , start , end = '' , title = '' ) : string => { const outline = EventStoreUtils . internalToolTipFormatterObject ( data ) ; return `<div class="tooltip-test">${ title . length > 0 ? title + '<br>' : '' }Start: ${ start } <br>${ end ? 'End: ' + end + '<br>' : '' }<b style="text-align: center;">Details</b><br>${ outline }</div>` ; } ; public static parseUpgradeAndRollback ( rollbackCompleteEvent , rollbackStartedEvent , items , startOfRange , group , targetVersionProperty ) { const rollbackEnd = rollbackCompleteEvent . timeStamp ; let rollbackStarted = startOfRange . toISOString ( ) ; if ( rollbackStartedEvent ) { rollbackStarted = rollbackStartedEvent . timeStamp ; const rollbackStartedDate = new Date ( rollbackEnd ) ; const upgradeDuration = rollbackCompleteEvent . eventProperties . OverallUpgradeElapsedTimeInMs ; const upgradeStart = new Date ( rollbackStartedDate . getTime ( ) - upgradeDuration ) . toISOString ( ) ; items . add ( { id : rollbackCompleteEvent . eventInstanceId + 'upgrade' , content : 'Upgrade rolling forward failed' , start : upgradeStart , end : rollbackStarted , group , type : 'range' , className : 'red' } ) ; } const label = `rolling back to ${ rollbackCompleteEvent . eventProperties [ targetVersionProperty ] }` ; items . add ( { id : rollbackCompleteEvent . eventInstanceId + label , content : label , start : rollbackStarted , end : rollbackEnd , group , type : 'range' , title : EventStoreUtils . tooltipFormat ( rollbackCompleteEvent . eventProperties , rollbackStarted , rollbackEnd ) , className : 'orange' } ) ; } public static parseUpgradeDomain ( event , items , group , targetVersionProperty ) { const end = event . timeStamp ; const endDate = new Date ( end ) ; const duration = event . eventProperties . UpgradeDomainElapsedTimeInMs ; const start = new Date ( endDate . getTime ( ) - duration ) . toISOString ( ) ; const label = event . eventProperties . UpgradeDomains ; items . add ( { id : event . eventInstanceId + label + event . eventProperties [ targetVersionProperty ] , content : label , start , end , group , type : 'range' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , start , end ) , className : 'green' } ) ; } public static parseUpgradeStarted ( event , items , endOfRange , group , targetVersionProperty ) { const end = endOfRange . toISOString ( ) ; const start = event . timeStamp ; const content = `Upgrading to ${ event . eventProperties [ targetVersionProperty ] }` ; items . add ( { id : event . eventInstanceId + content , content , start , end , group , type : 'range' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , start , end ) , className : 'blue' } ) ; } public static parseUpgradeCompleted ( event , items , group , targetVersionProperty ) { const rollBack = event . kind === 'ClusterUpgradeRollbackCompleted' ; const end = event . timeStamp ; const endDate = new Date ( end ) ; const duration = event . eventProperties . OverallUpgradeElapsedTimeInMs ; const start = new Date ( endDate . getTime ( ) - duration ) . toISOString ( ) ; const content = `${ rollBack ? 'Upgrade Rolling back' : 'Upgrade rolling forward' } to ${ event . eventProperties [ targetVersionProperty ] }` ; items . add ( { id : event . eventInstanceId + content , content , start , end , group , type : 'range' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , start , end ) , className : rollBack ? 'orange' : 'green' } ) ; } public static addSubGroups ( groups ) { groups . forEach ( group => { group . subgroupStack = { stack : true , noStack : false } ; groups . update ( group ) ; } ) ; } } export abstract class TimeLineGeneratorBase < T > { consume ( events , startOfRange , endOfRange ) { throw new Error ( 'NotImplementedError' ) ; } generateTimeLineData ( events , startOfRange ? , endOfRange ? , nestedGroupLabel ? ) { const data = this . consume ( events , startOfRange , endOfRange ) ; EventStoreUtils . addSubGroups ( data . groups ) ; if ( nestedGroupLabel ) { const nestedElementGroup = { id : nestedGroupLabel , content : nestedGroupLabel , nestedGroups : [ ] } ; let groupsAlreadyNested = [ ] ; data . groups . forEach ( group => { nestedElementGroup . nestedGroups . push ( group . id ) ; if ( group . nestedGroups ) { groupsAlreadyNested = groupsAlreadyNested . concat ( group . nestedGroups ) ; } } ) ; nestedElementGroup . nestedGroups = nestedElementGroup . nestedGroups . filter ( group => ! groupsAlreadyNested . includes ( group ) ) ; data . groups . add ( nestedElementGroup ) ; } return data ; } } export class ClusterTimelineGenerator extends TimeLineGeneratorBase < ClusterEvent > { static readonly upgradeDomainLabel = 'Cluster Upgrade Domains' ; static readonly clusterUpgradeLabel = 'Cluster Upgrades' ; static readonly seedNodeStatus = 'Seed Node Warnings' ; consume ( events , startOfRange , endOfRange ) { const items = new DataSet < DataItem > ( ) ; let previousClusterHealthReport ; let previousClusterUpgrade ; let upgradeClusterStarted ; const clusterRollBacks = { } ; events . forEach ( event => { if ( ( event . kind === 'ClusterUpgradeStarted' || event . kind === 'ClusterUpgradeRollbackStarted' ) && ! previousClusterUpgrade ) { upgradeClusterStarted = event ; } else if ( event . kind === 'ClusterUpgradeDomainCompleted' ) { EventStoreUtils . parseUpgradeDomain ( event , items , ClusterTimelineGenerator . upgradeDomainLabel , 'TargetClusterVersion' ) ; } else if ( event . kind === 'ClusterUpgradeCompleted' ) { EventStoreUtils . parseUpgradeCompleted ( event , items , ClusterTimelineGenerator . clusterUpgradeLabel , 'TargetClusterVersion' ) ; previousClusterUpgrade = event ; } else if ( event . kind === 'ClusterNewHealthReport' ) { this . parseSeedNodeStatus ( event , items , previousClusterHealthReport , endOfRange ) ; previousClusterHealthReport = event ; } if ( event . kind === 'ClusterUpgradeRollbackCompleted' ) { previousClusterUpgrade = event ; clusterRollBacks [ event . eventInstanceId ] = { complete : event } ; } else if ( event . kind === 'ClusterUpgradeRollbackStarted' && event . eventInstanceId in clusterRollBacks ) { clusterRollBacks [ event . eventInstanceId ] . start = event ; } } ) ; Object . keys ( clusterRollBacks ) . forEach ( eventInstanceId => { const data = clusterRollBacks [ eventInstanceId ] ; EventStoreUtils . parseUpgradeAndRollback ( data . complete , data . start , items , startOfRange , ClusterTimelineGenerator . clusterUpgradeLabel , 'TargetClusterVersion' ) ; } ) ; if ( upgradeClusterStarted ) { EventStoreUtils . parseUpgradeStarted ( upgradeClusterStarted , items , endOfRange , ClusterTimelineGenerator . clusterUpgradeLabel , 'TargetClusterVersion' ) ; } const groups = new DataSet < DataGroup > ( [ { id : ClusterTimelineGenerator . upgradeDomainLabel , content : ClusterTimelineGenerator . upgradeDomainLabel } , { id : ClusterTimelineGenerator . clusterUpgradeLabel , content : ClusterTimelineGenerator . clusterUpgradeLabel } , { id : ClusterTimelineGenerator . seedNodeStatus , content : ClusterTimelineGenerator . seedNodeStatus } ] ) ; return { groups , items } ; } parseSeedNodeStatus ( event , items , previousClusterHealthReport , endOfRange ) { if ( event . eventProperties . HealthState === 'Warning' ) { const end = previousClusterHealthReport ? previousClusterHealthReport . timeStamp : endOfRange . toISOString ( ) ; const content = `${ event . eventProperties . HealthState }` ; items . add ( { id : event . eventInstanceId + content , content , start : event . timeStamp , end , group : ClusterTimelineGenerator . seedNodeStatus , type : 'range' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , event . timeStamp , end ) , className : 'orange' } ) ; } } } export class NodeTimelineGenerator extends TimeLineGeneratorBase < NodeEvent > { static readonly NodesDownLabel = 'Node Down' ; static readonly NodesRemoved = 'Node Removed' ; static readonly NodesAdded = 'Node Added' ; static readonly transitions = [ 'NodeDeactivateStarted' ] ; consume ( events , startOfRange , endOfRange ) { const items = new DataSet < DataItem > ( ) ; const nodeUpEvents = { } ; const previousTransitions = { } ; const nodeDownDataItems = { } ; let potentiallyMissingEvents = false ; events . forEach ( event => { if ( event . category === 'StateTransition' ) { if ( event . kind === 'NodeDown' ) { const previousTransition = previousTransitions [ event . nodeName ] ; if ( previousTransition && previousTransition . kind !== 'NodeUp' ) { potentiallyMissingEvents = true ; } if ( event . nodeName in nodeUpEvents ) { delete nodeUpEvents [ event . nodeName ] ; } const end = previousTransition ? previousTransition . timeStamp : endOfRange . toISOString ( ) ; const start = event . timeStamp ; const label = `Node ${ event . nodeName } down` ; const item = { id : event . eventInstanceId + label , content : label , start , end , group : NodeTimelineGenerator . NodesDownLabel , type : 'range' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , start , end , label ) , className : 'red' , subgroup : 'stack' } ; if ( event . eventProperties . NodeInstance in nodeDownDataItems ) { items . add ( nodeDownDataItems [ event . eventProperties . NodeInstance ] ) ; } nodeDownDataItems [ event . eventProperties . NodeInstance ] = item ; } else if ( event . kind === 'NodeDeactivateCompleted' && event . eventProperties . EffectiveDeactivateIntent === 'RemoveNode' ) { const nodeDownEvent = nodeDownDataItems [ event . eventProperties . NodeInstance ] ; if ( nodeDownEvent ) { delete nodeDownDataItems [ event . eventProperties . NodeInstance ] ; potentiallyMissingEvents = true ; } } if ( event . kind === 'NodeUp' && event . eventProperties . LastNodeDownAt !== '1601-01-01T00:00:00Z' ) { previousTransitions [ event . nodeName ] = event ; nodeUpEvents [ event . nodeName ] = event ; } } if ( NodeTimelineGenerator . transitions . includes ( event . kind ) ) { previousTransitions [ event . nodeName ] = event ; } } ) ; Object . keys ( nodeDownDataItems ) . forEach ( key => { const item = nodeDownDataItems [ key ] ; items . add ( item ) ; } ) ; Object . keys ( nodeUpEvents ) . forEach ( key => { const event = nodeUpEvents [ key ] ; const label = `Node ${ event . nodeName } down` ; items . add ( { id : event . eventInstanceId + label , content : label , start : event . eventProperties . LastNodeDownAt , end : event . timeStamp , group : NodeTimelineGenerator . NodesDownLabel , type : 'range' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , event . eventProperties . LastNodeDownAt , event . timeStamp , label ) , className : 'red' , subgroup : 'stack' } ) ; } ) ; const groups = new DataSet < DataGroup > ( [ { id : NodeTimelineGenerator . NodesDownLabel , content : NodeTimelineGenerator . NodesDownLabel , subgroupStack : { stack : true } } , ] ) ; return { groups , items , potentiallyMissingEvents } ; } } export class ApplicationTimelineGenerator extends TimeLineGeneratorBase < ApplicationEvent > { static readonly upgradeDomainLabel = 'Application Upgrade Domains' ; static readonly applicationUpgradeLabel = 'Application Upgrades' ; static readonly applicationPrcoessExitedLabel = 'Application Process Exited' ; consume ( events , startOfRange , endOfRange ) { const items = new DataSet < DataItem > ( ) ; let previousApplicationUpgrade ; let upgradeApplicationStarted ; const applicationRollBacks = { } ; const processExitedGroups = { } ; events . forEach ( event => { if ( ( event . kind === 'ApplicationUpgradeStarted' || event . kind === 'ApplicationUpgradeRollbackStarted' ) && ! previousApplicationUpgrade ) { upgradeApplicationStarted = event ; } else if ( event . kind === 'ApplicationUpgradeDomainCompleted' ) { EventStoreUtils . parseUpgradeDomain ( event , items , ApplicationTimelineGenerator . upgradeDomainLabel , 'ApplicationTypeVersion' ) ; } else if ( event . kind === 'ApplicationUpgradeCompleted' ) { EventStoreUtils . parseUpgradeCompleted ( event , items , ApplicationTimelineGenerator . applicationUpgradeLabel , 'ApplicationTypeVersion' ) ; previousApplicationUpgrade = event ; } else if ( event . kind === 'ApplicationProcessExited' ) { this . parseApplicationProcessExited ( event , items , processExitedGroups ) ; } if ( event . kind === 'ApplicationUpgradeRollbackCompleted' ) { previousApplicationUpgrade = event ; applicationRollBacks [ event . eventInstanceId ] = { complete : event } ; } else if ( event . kind === 'ApplicationUpgradeRollbackStarted' && event . eventInstanceId in applicationRollBacks ) { applicationRollBacks [ event . eventInstanceId ] . start = event ; } } ) ; Object . keys ( applicationRollBacks ) . forEach ( eventInstanceId => { const data = applicationRollBacks [ eventInstanceId ] ; EventStoreUtils . parseUpgradeAndRollback ( data . complete , data . start , items , startOfRange , ApplicationTimelineGenerator . applicationUpgradeLabel , 'ApplicationTypeVersion' ) ; } ) ; if ( upgradeApplicationStarted ) { EventStoreUtils . parseUpgradeStarted ( upgradeApplicationStarted , items , endOfRange , ApplicationTimelineGenerator . applicationUpgradeLabel , 'ApplicationTypeVersion' ) ; } const groups = new DataSet < DataGroup > ( [ { id : ApplicationTimelineGenerator . upgradeDomainLabel , content : ApplicationTimelineGenerator . upgradeDomainLabel } , { id : ApplicationTimelineGenerator . applicationUpgradeLabel , content : ApplicationTimelineGenerator . applicationUpgradeLabel } , ] ) ; const nestedApplicationProcessExited = { id : ApplicationTimelineGenerator . applicationPrcoessExitedLabel , nestedGroups : [ ] , content : ApplicationTimelineGenerator . applicationPrcoessExitedLabel , } ; Object . keys ( processExitedGroups ) . forEach ( groupName => { nestedApplicationProcessExited . nestedGroups . push ( groupName ) ; groups . add ( processExitedGroups [ groupName ] ) ; } ) ; groups . add ( nestedApplicationProcessExited ) ; return { groups , items } ; } parseApplicationProcessExited ( event , items , processExitedGroups ) { const groupLabel = `${ event . eventProperties . ServicePackageName }` ; processExitedGroups [ groupLabel ] = { id : groupLabel , content : groupLabel } ; const start = event . timeStamp ; const label = event . eventProperties . ExeName ; items . add ( { id : event . eventInstanceId + label , content : '' , start , group : groupLabel , type : 'point' , className : event . eventProperties . UnexpectedTermination ? 'red-point' : 'green-point' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , start , null , 'Primary swap to ' + label ) , } ) ; } } export class PartitionTimelineGenerator extends TimeLineGeneratorBase < PartitionEvent > { static readonly swapPrimaryLabel = 'Primary Swap' ; static readonly swapPrimaryDurations = 'Swap Primary phases' ; consume ( events , startOfRange , endOfRange ) { const items = new DataSet < DataItem > ( ) ; events . forEach ( event => { if ( event . category === 'StateTransition' && event . eventProperties . ReconfigType === 'SwapPrimary' ) { const end = event . timeStamp ; const endDate = new Date ( end ) ; const duration = event . eventProperties . TotalDurationMs ; const start = new Date ( endDate . getTime ( ) - duration ) . toISOString ( ) ; const label = event . eventProperties . NodeName ; items . add ( { id : event . eventInstanceId + label , content : label , start , end , group : PartitionTimelineGenerator . swapPrimaryLabel , type : 'range' , title : EventStoreUtils . tooltipFormat ( event . eventProperties , start , end , 'Primary swap to ' + label ) , className : 'green' } ) ; } } ) ; const groups = new DataSet < DataGroup > ( [ { id : PartitionTimelineGenerator . swapPrimaryLabel , content : PartitionTimelineGenerator . swapPrimaryLabel } , ] ) ; return { groups , items } ; } } export class RepairTaskTimelineGenerator extends TimeLineGeneratorBase < RepairTask > { consume ( tasks , startOfRange , endOfRange ) { const items = new DataSet < DataItem > ( ) ; const groups = new DataSet < DataGroup > ( ) ; tasks . forEach ( task => { items . add ( { id : task . raw . TaskId , content : task . raw . TaskId , start : task . startTime , end : task . inProgress ? new Date ( ) : new Date ( task . raw . History . CompletedUtcTimestamp ) , type : 'range' , group : 'job' , subgroup : 'stack' , className : task . inProgress ? 'blue' : task . raw . ResultStatus === 'Succeeded' ? 'green' : 'red' , title : EventStoreUtils . tooltipFormat ( task . raw , new Date ( task . raw . History . ExecutingUtcTimestamp ) . toLocaleString ( ) , new Date ( task . raw . History . CompletedUtcTimestamp ) . toLocaleString ( ) ) , } ) ; } ) ; groups . add ( { id : 'job' , content : 'Job History' , subgroupStack : { stack : true } } ) ; return { groups , items , } ; } } /**
 * Take a csv string and parses the event into groups with nested references to properties specified in the query string.
 * i.e "Category, Kind"
 *   The layout is set to look like
 *   Category1
 *       Category1 - kind 1
 *       category1 - kind 2
 *    Category2
 *       Category2 - kind 1
 *       category2 - kind 2
 */ function parseAndAddGroupIdByString ( event , groupIds , query ) { const properties = query . split ( ',' ) ; let constructedPath = '' ; for ( let i = 0 ; i < properties . length ; i ++ ) { const prop = properties [ i ] ; if ( prop in event . raw ) { const currentPath = `${ constructedPath } ${ event . raw [ prop ] }` ; if ( findIndex ( groupIds , ( g ) => g . id === currentPath ) === - 1 ) { const content = padStart ( '' , i * 3 ) + event . raw [ prop ] . toString ( ) ; const childGroup = { id : currentPath , content } ; if ( ( i + 1 ) < properties . length ) { childGroup . nestedGroups = [ ] ; } if ( i > 0 ) { const parentGroup = groupIds [ findIndex ( groupIds , ( g ) => g . id === constructedPath ) ] ; parentGroup . nestedGroups . push ( childGroup . id ) ; } groupIds . push ( childGroup ) ; } constructedPath = currentPath ; } else { i = properties . length ; constructedPath = '' ; } } return constructedPath ; } export function parseEventsGenerically ( events , textSearch = '' ) { const items = new DataSet < DataItem > ( ) ; const groupIds = [ ] ; events . forEach ( ( event , index ) => { const groupId = parseAndAddGroupIdByString ( event , groupIds , textSearch ) ; let color = 'white' ; if ( 'Status' in event . eventProperties ) { try { const status = event . eventProperties . Status ; if ( status === 'Ok' ) { color = 'green' ; } else if ( status === 'Warning' ) { color = 'orange' ; } else if ( status === 'Error' ) { color = 'red' ; } } catch ( e ) { } } else { if ( HtmlUtils . eventTypesUtil . isResolved ( event ) ) { color = 'green' ; } else if ( HtmlUtils . eventTypesUtil . isWarning ( event ) ) { color = 'orange' ; } else if ( HtmlUtils . eventTypesUtil . isError ( event ) ) { color = 'red' ; } } const item = { content : '' , id : index , start : event . timeStamp , group : groupId , type : 'point' , title : EventStoreUtils . tooltipFormat ( event . raw , event . timeStamp ) , className : `${ color }-point` , subgroup : 'noStack' } ; if ( 'Duration' in event . eventProperties ) { if ( 'Description' in event . eventProperties ) { try { item . content = event . eventProperties . Description ; } catch ( e ) { } } try { const duration = event . eventProperties . Duration ; const end = event . timeStamp ; const endDate = new Date ( end ) ; const start = new Date ( endDate . getTime ( ) - duration ) . toISOString ( ) ; if ( duration < 0 ) { item . start = end ; item . end = start ; item . title = EventStoreUtils . tooltipFormat ( event . raw , start , end , item . content ) ; } else { item . start = start ; item . end = end ; item . title = EventStoreUtils . tooltipFormat ( event . raw , end , start , item . content ) ; } item . type = 'range' ; item . className = color ; item . subgroup = 'stack' ; } catch ( e ) { } } items . add ( item ) ; } ) ; const groups = new DataSet < DataGroup > ( groupIds ) ; EventStoreUtils . addSubGroups ( groups ) ; return { groups , items } ; }