Skip to content

Commit d4397cc

Browse files
feat(graph): add service graph visualization and related data structures
1 parent 3d33ecd commit d4397cc

4 files changed

Lines changed: 223 additions & 0 deletions

File tree

pkg/console/handler/service.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,22 @@ func ServiceConfigArgumentRoutePUT(ctx consolectx.Context) gin.HandlerFunc {
229229
c.JSON(http.StatusOK, model.NewSuccessResp(nil))
230230
}
231231
}
232+
233+
// GetServiceGraph returns the service graph in a cross-linked list structure for visualization
234+
func GetServiceGraph(ctx consolectx.Context) gin.HandlerFunc {
235+
return func(c *gin.Context) {
236+
req := &model.ServiceGraphReq{}
237+
if err := c.ShouldBindQuery(req); err != nil {
238+
c.JSON(http.StatusBadRequest, model.NewErrorResp(err.Error()))
239+
return
240+
}
241+
242+
resp, err := service.SearchServiceAsCrossLinkedList(ctx, req)
243+
if err != nil {
244+
util.HandleServiceError(c, err)
245+
return
246+
}
247+
248+
c.JSON(http.StatusOK, model.NewSuccessResp(resp))
249+
}
250+
}

pkg/console/model/graph.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package model
2+
3+
import (
4+
meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
5+
)
6+
7+
// GraphNode represents a node in the graph for AntV G6
8+
type GraphNode struct {
9+
ID string `json:"id"`
10+
Label string `json:"label"`
11+
Data interface{} `json:"data,omitempty"` // Additional data for the node
12+
}
13+
14+
// GraphEdge represents an edge in the graph for AntV G6
15+
type GraphEdge struct {
16+
Source string `json:"source"`
17+
Target string `json:"target"`
18+
Data map[string]interface{} `json:"data,omitempty"` // Additional data for the edge
19+
}
20+
21+
// GraphData represents the complete graph structure for AntV G6
22+
type GraphData struct {
23+
Nodes []GraphNode `json:"nodes"`
24+
Edges []GraphEdge `json:"edges"`
25+
}
26+
27+
// CrossNode represents a node in the cross-linked list structure
28+
type CrossNode struct {
29+
Instance *meshresource.InstanceResource
30+
Next *CrossNode // pointer to next node in the same row
31+
Down *CrossNode // pointer to next node in the same column
32+
}
33+
34+
// CrossLinkedListGraph represents the cross-linked list structure as a directed graph
35+
type CrossLinkedListGraph struct {
36+
Head *CrossNode
37+
Rows int // number of rows
38+
Cols int // number of columns
39+
}
40+
41+
// ServiceGraphReq represents the request parameters for fetching the service graph
42+
type ServiceGraphReq struct {
43+
ServiceName string `json:"serviceName" form:"serviceName"`
44+
Mesh string `json:"mesh" form:"mesh" binding:"required"`
45+
}

pkg/console/router/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) {
9999
service := router.Group("/service")
100100
service.GET("/distribution", handler.GetServiceTabDistribution(ctx))
101101
service.GET("/search", handler.SearchServices(ctx))
102+
service.GET("/graph", handler.GetServiceGraph(ctx))
102103
//service.GET("/detail", handler.GetServiceDetail(ctx))
103104
//service.GET("/interfaces", handler.GetServiceInterfaces(ctx))
104105
}

pkg/console/service/service.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,3 +522,161 @@ func isArgumentRoute(condition string) bool {
522522
}
523523
return false
524524
}
525+
526+
// SearchServiceAsCrossLinkedList builds a service dependency graph.
527+
// If serviceName is provided, it finds applications that consume or provide that specific service.
528+
// If serviceName is empty, it returns all service dependencies in the mesh.
529+
// It constructs nodes (applications) and edges (dependencies) for graph visualization.
530+
func SearchServiceAsCrossLinkedList(ctx consolectx.Context, req *model.ServiceGraphReq) (*model.GraphData, error) {
531+
// Build indexes conditionally based on whether serviceName is provided
532+
consumerIndexes := map[string]string{
533+
index.ByMeshIndex: req.Mesh,
534+
}
535+
providerIndexes := map[string]string{
536+
index.ByMeshIndex: req.Mesh,
537+
}
538+
539+
// Only filter by serviceName if it's provided
540+
if req.ServiceName != "" {
541+
consumerIndexes[index.ByServiceConsumerServiceName] = req.ServiceName
542+
providerIndexes[index.ByServiceProviderServiceName] = req.ServiceName
543+
}
544+
545+
// Use ListByIndexes instead of PageListByIndexes to get all related resources
546+
// since we need complete dependency graph, not paginated results
547+
consumers, err := manager.ListByIndexes[*meshresource.ServiceConsumerMetadataResource](
548+
ctx.ResourceManager(),
549+
meshresource.ServiceConsumerMetadataKind,
550+
consumerIndexes)
551+
if err != nil {
552+
logger.Errorf("get service consumer for mesh %s failed, cause: %v", req.Mesh, err)
553+
return nil, bizerror.New(bizerror.InternalError, "get service consumer failed, please try again")
554+
}
555+
556+
providers, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource](
557+
ctx.ResourceManager(),
558+
meshresource.ServiceProviderMetadataKind,
559+
providerIndexes)
560+
if err != nil {
561+
logger.Errorf("get service provider for mesh %s failed, cause: %v", req.Mesh, err)
562+
return nil, bizerror.New(bizerror.InternalError, "get service provider failed, please try again")
563+
}
564+
565+
// Collect all unique applications (both consumers and providers)
566+
consumerApps := make(map[string]bool)
567+
for _, consumer := range consumers {
568+
if consumer.Spec != nil {
569+
consumerApps[consumer.Spec.ConsumerAppName] = true
570+
}
571+
}
572+
573+
providerApps := make(map[string]bool)
574+
for _, provider := range providers {
575+
if provider.Spec != nil {
576+
providerApps[provider.Spec.ProviderAppName] = true
577+
}
578+
}
579+
580+
allApps := make(map[string]bool)
581+
for app := range consumerApps {
582+
allApps[app] = true
583+
}
584+
for app := range providerApps {
585+
allApps[app] = true
586+
}
587+
588+
nodes := make([]model.GraphNode, 0, len(allApps))
589+
edges := make([]model.GraphEdge, 0)
590+
591+
// Build app to service instances mapping
592+
// For providers: map providerAppName -> list of instances providing this service
593+
// For consumers: map consumerAppName -> empty list (consumers don't provide instances)
594+
appInstances := make(map[string][]*meshresource.InstanceResource)
595+
596+
// Get all instances for the mesh first
597+
allInstances, err := manager.ListByIndexes[*meshresource.InstanceResource](
598+
ctx.ResourceManager(),
599+
meshresource.InstanceKind,
600+
map[string]string{
601+
index.ByMeshIndex: req.Mesh,
602+
})
603+
if err != nil {
604+
logger.Errorf("get instances for mesh %s failed, cause: %v", req.Mesh, err)
605+
}
606+
607+
// Build app -> instances mapping
608+
for _, instance := range allInstances {
609+
if instance.Spec != nil && instance.Spec.AppName != "" {
610+
appInstances[instance.Spec.AppName] = append(appInstances[instance.Spec.AppName], instance)
611+
}
612+
}
613+
614+
// Build nodes for each app
615+
// Provider nodes: data contains instances providing this service
616+
// Consumer nodes: data is nil (consumers don't provide instances)
617+
for appName := range allApps {
618+
var instanceData interface{}
619+
620+
instances := make([]interface{}, 0)
621+
if appInsts, ok := appInstances[appName]; ok {
622+
for _, instance := range appInsts {
623+
instances = append(instances, toInstanceData(instance))
624+
}
625+
}
626+
instanceData = instances
627+
628+
nodes = append(nodes, model.GraphNode{
629+
ID: appName,
630+
Label: appName,
631+
Data: instanceData,
632+
})
633+
}
634+
635+
// Build edges between consumers and providers
636+
// Only create edges between apps that actually have the service relationship
637+
for _, consumer := range consumers {
638+
if consumer.Spec != nil {
639+
for _, provider := range providers {
640+
if provider.Spec != nil {
641+
// This is where you should check if the provider's service name and the consumer's service name match.
642+
if consumer.Spec.ServiceName != provider.Spec.ServiceName {
643+
continue
644+
}
645+
edges = append(edges, model.GraphEdge{
646+
Source: consumer.Spec.ConsumerAppName,
647+
Target: provider.Spec.ProviderAppName,
648+
Data: map[string]interface{}{
649+
"type": "dependency",
650+
"serviceName": req.ServiceName,
651+
"consumerApp": consumer.Spec.ConsumerAppName,
652+
"providerApp": provider.Spec.ProviderAppName,
653+
},
654+
})
655+
}
656+
}
657+
}
658+
}
659+
660+
return &model.GraphData{
661+
Nodes: nodes,
662+
Edges: edges,
663+
}, nil
664+
}
665+
666+
func toInstanceData(instance *meshresource.InstanceResource) map[string]interface{} {
667+
if instance == nil || instance.Spec == nil {
668+
return nil
669+
}
670+
671+
data := map[string]interface{}{
672+
"appName": instance.Spec.AppName,
673+
"ip": instance.Spec.Ip,
674+
"name": instance.Spec.Name,
675+
"protocol": instance.Spec.Protocol,
676+
"qosPort": instance.Spec.QosPort,
677+
"rpcPort": instance.Spec.RpcPort,
678+
"tags": instance.Spec.Tags,
679+
}
680+
681+
return data
682+
}

0 commit comments

Comments
 (0)