import { sortBy , pickBy , identity } from 'lodash' ; import { ValuesType } from 'utility-types' ; import { SERVICE_NAME , SPAN_DESTINATION_SERVICE_RESOURCE , SPAN_TYPE , SPAN_SUBTYPE , } from '../../../common/elasticsearch_fieldnames' ; import { Connection , ConnectionNode , ServiceConnectionNode , ExternalConnectionNode , ConnectionElement , } from '../../../common/service_map' ; import { ConnectionsResponse , ServicesResponse } from './get_service_map' ; import { ServiceAnomaliesResponse } from './get_service_anomalies' ; import { groupResourceNodes } from './group_resource_nodes' ; function getConnectionNodeId ( node ) { if ( 'span.destination.service.resource' in node ) { return `>${ node [ SPAN_DESTINATION_SERVICE_RESOURCE ] }` ; } return node [ SERVICE_NAME ] ; } function getConnectionId ( connection ) { return `${ getConnectionNodeId ( connection . source ) }~${ getConnectionNodeId ( connection . destination ) }` ; } export function getAllNodes ( services , connections ) { const allNodes = connections . flatMap ( ( connection ) => [ connection . source , connection . destination ] ) . map ( ( node ) => ( { ... node , id : getConnectionNodeId ( node ) } ) ) . concat ( services . map ( ( service ) => ( { ... service , id : service [ SERVICE_NAME ] , } ) ) ) ; return allNodes ; } export function getServiceNodes ( allNodes ) { const serviceNodes = allNodes . filter ( ( node ) => SERVICE_NAME in node ) as ServiceConnectionNode [ ] ; return serviceNodes ; } export type ServiceMapResponse = ConnectionsResponse & { services : ServicesResponse ; anomalies : ServiceAnomaliesResponse ; } ; export function transformServiceMapResponses ( response ) { const { discoveredServices , services , connections , anomalies } = response ; const allNodes = getAllNodes ( services , connections ) ; const serviceNodes = getServiceNodes ( allNodes ) ; const externalNodes = allNodes . filter ( ( node ) => SPAN_DESTINATION_SERVICE_RESOURCE in node ) as ExternalConnectionNode [ ] ; const nodeMap = allNodes . reduce ( ( map , node ) => { if ( ! node . id || map [ node . id ] ) { return map ; } const matchedService = discoveredServices . find ( ( { from } ) => { if ( 'span.destination.service.resource' in node ) { return ( node [ SPAN_DESTINATION_SERVICE_RESOURCE ] === from [ SPAN_DESTINATION_SERVICE_RESOURCE ] ) ; } return false ; } ) ?. to ; let serviceName = matchedService ?. [ SERVICE_NAME ] ; if ( ! serviceName && 'service.name' in node ) { serviceName = node [ SERVICE_NAME ] ; } const matchedServiceNodes = serviceNodes . filter ( ( serviceNode ) => serviceNode [ SERVICE_NAME ] === serviceName ) . map ( ( serviceNode ) => pickBy ( serviceNode , identity ) ) ; const mergedServiceNode = Object . assign ( { } , ... matchedServiceNodes ) ; const serviceAnomalyStats = serviceName ? anomalies . serviceAnomalies . find ( ( item ) => item . serviceName === serviceName ) : null ; if ( matchedServiceNodes . length ) { return { ... map , [ node . id ] : { id : matchedServiceNodes [ 0 ] [ SERVICE_NAME ] , ... mergedServiceNode , ... ( serviceAnomalyStats ? { serviceAnomalyStats } : null ) , } , } ; } const allMatchedExternalNodes = externalNodes . filter ( ( n ) => n . id === node . id ) ; const firstMatchedNode = allMatchedExternalNodes [ 0 ] ; return { ... map , [ node . id ] : { ... firstMatchedNode , label : firstMatchedNode [ SPAN_DESTINATION_SERVICE_RESOURCE ] , [ SPAN_TYPE ] : allMatchedExternalNodes . map ( ( n ) => n [ SPAN_TYPE ] ) . sort ( ) [ 0 ] , [ SPAN_SUBTYPE ] : allMatchedExternalNodes . map ( ( n ) => n [ SPAN_SUBTYPE ] ) . sort ( ) [ 0 ] , } , } ; } , { } as Record < string , ConnectionNode > ) ; function getConnectionNode ( node ) { return nodeMap [ getConnectionNodeId ( node ) ] ; } const mappedConnections = connections . map ( ( connection ) => { const sourceData = getConnectionNode ( connection . source ) ; const targetData = getConnectionNode ( connection . destination ) ; return { source : sourceData . id , target : targetData . id , id : getConnectionId ( { source : sourceData , destination : targetData } ) , sourceData , targetData , } ; } ) . filter ( ( connection ) => connection . source !== connection . target ) ; const nodes = mappedConnections . flatMap ( ( connection ) => [ connection . sourceData , connection . targetData ] ) . concat ( serviceNodes ) ; const dedupedNodes = [ ] ; nodes . forEach ( ( node ) => { if ( ! dedupedNodes . find ( ( dedupedNode ) => node . id === dedupedNode . id ) ) { dedupedNodes . push ( node ) ; } } ) ; type ConnectionWithId = ValuesType < typeof mappedConnections > ; const connectionsById = mappedConnections . reduce ( ( connectionMap , connection ) => { return { ... connectionMap , [ connection . id ] : connection , } ; } , { } as Record < string , ConnectionWithId > ) ; const dedupedConnections = ( sortBy ( Object . values ( connectionsById ) , 'id' ) as ConnectionWithId [ ] ) . reduce < Array < ConnectionWithId & { bidirectional ? : boolean ; isInverseEdge ? : boolean ; } > > ( ( prev , connection ) => { const reversedConnection = prev . find ( ( c ) => c . target === connection . source && c . source === connection . target ) ; if ( reversedConnection ) { reversedConnection . bidirectional = true ; return prev . concat ( { ... connection , isInverseEdge : true , } ) ; } return prev . concat ( connection ) ; } , [ ] ) ; const elements = [ ... dedupedConnections , ... dedupedNodes , ] . map ( ( element ) => ( { data : element , } ) ) ; return groupResourceNodes ( { elements } ) ; }