From d4397cc647fbdc968670e083bc7b0fec6c45cc80 Mon Sep 17 00:00:00 2001 From: Comrade Yi Date: Sun, 8 Feb 2026 21:39:11 +0800 Subject: [PATCH 1/5] feat(graph): add service graph visualization and related data structures --- pkg/console/handler/service.go | 19 ++++ pkg/console/model/graph.go | 45 ++++++++++ pkg/console/router/router.go | 1 + pkg/console/service/service.go | 158 +++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 pkg/console/model/graph.go diff --git a/pkg/console/handler/service.go b/pkg/console/handler/service.go index 936b796f5..19d1e4028 100644 --- a/pkg/console/handler/service.go +++ b/pkg/console/handler/service.go @@ -229,3 +229,22 @@ func ServiceConfigArgumentRoutePUT(ctx consolectx.Context) gin.HandlerFunc { c.JSON(http.StatusOK, model.NewSuccessResp(nil)) } } + +// GetServiceGraph returns the service graph in a cross-linked list structure for visualization +func GetServiceGraph(ctx consolectx.Context) gin.HandlerFunc { + return func(c *gin.Context) { + req := &model.ServiceGraphReq{} + if err := c.ShouldBindQuery(req); err != nil { + c.JSON(http.StatusBadRequest, model.NewErrorResp(err.Error())) + return + } + + resp, err := service.SearchServiceAsCrossLinkedList(ctx, req) + if err != nil { + util.HandleServiceError(c, err) + return + } + + c.JSON(http.StatusOK, model.NewSuccessResp(resp)) + } +} diff --git a/pkg/console/model/graph.go b/pkg/console/model/graph.go new file mode 100644 index 000000000..278eee7f9 --- /dev/null +++ b/pkg/console/model/graph.go @@ -0,0 +1,45 @@ +package model + +import ( + meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1" +) + +// GraphNode represents a node in the graph for AntV G6 +type GraphNode struct { + ID string `json:"id"` + Label string `json:"label"` + Data interface{} `json:"data,omitempty"` // Additional data for the node +} + +// GraphEdge represents an edge in the graph for AntV G6 +type GraphEdge struct { + Source string `json:"source"` + Target string `json:"target"` + Data map[string]interface{} `json:"data,omitempty"` // Additional data for the edge +} + +// GraphData represents the complete graph structure for AntV G6 +type GraphData struct { + Nodes []GraphNode `json:"nodes"` + Edges []GraphEdge `json:"edges"` +} + +// CrossNode represents a node in the cross-linked list structure +type CrossNode struct { + Instance *meshresource.InstanceResource + Next *CrossNode // pointer to next node in the same row + Down *CrossNode // pointer to next node in the same column +} + +// CrossLinkedListGraph represents the cross-linked list structure as a directed graph +type CrossLinkedListGraph struct { + Head *CrossNode + Rows int // number of rows + Cols int // number of columns +} + +// ServiceGraphReq represents the request parameters for fetching the service graph +type ServiceGraphReq struct { + ServiceName string `json:"serviceName" form:"serviceName"` + Mesh string `json:"mesh" form:"mesh" binding:"required"` +} diff --git a/pkg/console/router/router.go b/pkg/console/router/router.go index c68c763d1..1767cd9e2 100644 --- a/pkg/console/router/router.go +++ b/pkg/console/router/router.go @@ -99,6 +99,7 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) { service := router.Group("/service") service.GET("/distribution", handler.GetServiceTabDistribution(ctx)) service.GET("/search", handler.SearchServices(ctx)) + service.GET("/graph", handler.GetServiceGraph(ctx)) //service.GET("/detail", handler.GetServiceDetail(ctx)) //service.GET("/interfaces", handler.GetServiceInterfaces(ctx)) } diff --git a/pkg/console/service/service.go b/pkg/console/service/service.go index 85d58a1a8..cc8cb8b0d 100644 --- a/pkg/console/service/service.go +++ b/pkg/console/service/service.go @@ -522,3 +522,161 @@ func isArgumentRoute(condition string) bool { } return false } + +// SearchServiceAsCrossLinkedList builds a service dependency graph. +// If serviceName is provided, it finds applications that consume or provide that specific service. +// If serviceName is empty, it returns all service dependencies in the mesh. +// It constructs nodes (applications) and edges (dependencies) for graph visualization. +func SearchServiceAsCrossLinkedList(ctx consolectx.Context, req *model.ServiceGraphReq) (*model.GraphData, error) { + // Build indexes conditionally based on whether serviceName is provided + consumerIndexes := map[string]string{ + index.ByMeshIndex: req.Mesh, + } + providerIndexes := map[string]string{ + index.ByMeshIndex: req.Mesh, + } + + // Only filter by serviceName if it's provided + if req.ServiceName != "" { + consumerIndexes[index.ByServiceConsumerServiceName] = req.ServiceName + providerIndexes[index.ByServiceProviderServiceName] = req.ServiceName + } + + // Use ListByIndexes instead of PageListByIndexes to get all related resources + // since we need complete dependency graph, not paginated results + consumers, err := manager.ListByIndexes[*meshresource.ServiceConsumerMetadataResource]( + ctx.ResourceManager(), + meshresource.ServiceConsumerMetadataKind, + consumerIndexes) + if err != nil { + logger.Errorf("get service consumer for mesh %s failed, cause: %v", req.Mesh, err) + return nil, bizerror.New(bizerror.InternalError, "get service consumer failed, please try again") + } + + providers, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource]( + ctx.ResourceManager(), + meshresource.ServiceProviderMetadataKind, + providerIndexes) + if err != nil { + logger.Errorf("get service provider for mesh %s failed, cause: %v", req.Mesh, err) + return nil, bizerror.New(bizerror.InternalError, "get service provider failed, please try again") + } + + // Collect all unique applications (both consumers and providers) + consumerApps := make(map[string]bool) + for _, consumer := range consumers { + if consumer.Spec != nil { + consumerApps[consumer.Spec.ConsumerAppName] = true + } + } + + providerApps := make(map[string]bool) + for _, provider := range providers { + if provider.Spec != nil { + providerApps[provider.Spec.ProviderAppName] = true + } + } + + allApps := make(map[string]bool) + for app := range consumerApps { + allApps[app] = true + } + for app := range providerApps { + allApps[app] = true + } + + nodes := make([]model.GraphNode, 0, len(allApps)) + edges := make([]model.GraphEdge, 0) + + // Build app to service instances mapping + // For providers: map providerAppName -> list of instances providing this service + // For consumers: map consumerAppName -> empty list (consumers don't provide instances) + appInstances := make(map[string][]*meshresource.InstanceResource) + + // Get all instances for the mesh first + allInstances, err := manager.ListByIndexes[*meshresource.InstanceResource]( + ctx.ResourceManager(), + meshresource.InstanceKind, + map[string]string{ + index.ByMeshIndex: req.Mesh, + }) + if err != nil { + logger.Errorf("get instances for mesh %s failed, cause: %v", req.Mesh, err) + } + + // Build app -> instances mapping + for _, instance := range allInstances { + if instance.Spec != nil && instance.Spec.AppName != "" { + appInstances[instance.Spec.AppName] = append(appInstances[instance.Spec.AppName], instance) + } + } + + // Build nodes for each app + // Provider nodes: data contains instances providing this service + // Consumer nodes: data is nil (consumers don't provide instances) + for appName := range allApps { + var instanceData interface{} + + instances := make([]interface{}, 0) + if appInsts, ok := appInstances[appName]; ok { + for _, instance := range appInsts { + instances = append(instances, toInstanceData(instance)) + } + } + instanceData = instances + + nodes = append(nodes, model.GraphNode{ + ID: appName, + Label: appName, + Data: instanceData, + }) + } + + // Build edges between consumers and providers + // Only create edges between apps that actually have the service relationship + for _, consumer := range consumers { + if consumer.Spec != nil { + for _, provider := range providers { + if provider.Spec != nil { + // This is where you should check if the provider's service name and the consumer's service name match. + if consumer.Spec.ServiceName != provider.Spec.ServiceName { + continue + } + edges = append(edges, model.GraphEdge{ + Source: consumer.Spec.ConsumerAppName, + Target: provider.Spec.ProviderAppName, + Data: map[string]interface{}{ + "type": "dependency", + "serviceName": req.ServiceName, + "consumerApp": consumer.Spec.ConsumerAppName, + "providerApp": provider.Spec.ProviderAppName, + }, + }) + } + } + } + } + + return &model.GraphData{ + Nodes: nodes, + Edges: edges, + }, nil +} + +func toInstanceData(instance *meshresource.InstanceResource) map[string]interface{} { + if instance == nil || instance.Spec == nil { + return nil + } + + data := map[string]interface{}{ + "appName": instance.Spec.AppName, + "ip": instance.Spec.Ip, + "name": instance.Spec.Name, + "protocol": instance.Spec.Protocol, + "qosPort": instance.Spec.QosPort, + "rpcPort": instance.Spec.RpcPort, + "tags": instance.Spec.Tags, + } + + return data +} From 13f7855bc669426756c0a6f5b1dda22e8539f98d Mon Sep 17 00:00:00 2001 From: Comrade Yi Date: Tue, 10 Feb 2026 12:47:06 +0800 Subject: [PATCH 2/5] feat(service): enhance SearchServiceAsCrossLinkedList to merge duplicate edges and add logging for nil specs --- pkg/console/service/service.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/console/service/service.go b/pkg/console/service/service.go index cc8cb8b0d..3e9a13e82 100644 --- a/pkg/console/service/service.go +++ b/pkg/console/service/service.go @@ -631,6 +631,7 @@ func SearchServiceAsCrossLinkedList(ctx consolectx.Context, req *model.ServiceGr Data: instanceData, }) } + edgeKeyMap := make(map[string]struct{}) // Build edges between consumers and providers // Only create edges between apps that actually have the service relationship @@ -642,6 +643,14 @@ func SearchServiceAsCrossLinkedList(ctx consolectx.Context, req *model.ServiceGr if consumer.Spec.ServiceName != provider.Spec.ServiceName { continue } + // If there are two instances, such as p1->c1 and p2->c1, two edges will be generated, which need to be merged. + // Merging logic: Only one edge is kept for identical source and target. + // However, using a loop would result in three levels of nesting. Therefore, an auxiliary map is created for efficient filtering. + edgeKey := consumer.Spec.ConsumerAppName + "->" + provider.Spec.ProviderAppName + if _, exists := edgeKeyMap[edgeKey]; exists { + continue + } + edgeKeyMap[edgeKey] = struct{}{} edges = append(edges, model.GraphEdge{ Source: consumer.Spec.ConsumerAppName, Target: provider.Spec.ProviderAppName, @@ -652,8 +661,12 @@ func SearchServiceAsCrossLinkedList(ctx consolectx.Context, req *model.ServiceGr "providerApp": provider.Spec.ProviderAppName, }, }) + } else { + logger.Warnf("provider spec is nil for provider resource: %s", provider.Name) } } + } else { + logger.Warnf("consumer spec is nil for consumer resource: %s", consumer.Name) } } From c9e76b5b209004447e55ba7b61b54df4922180bd Mon Sep 17 00:00:00 2001 From: Comrade Yi Date: Tue, 10 Feb 2026 13:12:47 +0800 Subject: [PATCH 3/5] fix(license): restore license header in graph.go --- pkg/console/model/graph.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/console/model/graph.go b/pkg/console/model/graph.go index 278eee7f9..67bc913ea 100644 --- a/pkg/console/model/graph.go +++ b/pkg/console/model/graph.go @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package model import ( From 82474b64d2f998badfcc5c3dd2a4d11ace1eced5 Mon Sep 17 00:00:00 2001 From: Comrade Yi <119987662+ambiguous-pointer@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:11:48 +0800 Subject: [PATCH 4/5] Apply suggestion from @Copilot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 确定无地方使用 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/console/handler/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/console/handler/service.go b/pkg/console/handler/service.go index 19d1e4028..041a0c2d0 100644 --- a/pkg/console/handler/service.go +++ b/pkg/console/handler/service.go @@ -230,7 +230,7 @@ func ServiceConfigArgumentRoutePUT(ctx consolectx.Context) gin.HandlerFunc { } } -// GetServiceGraph returns the service graph in a cross-linked list structure for visualization +// GetServiceGraph returns the service graph as graph data (nodes and edges) for visualization func GetServiceGraph(ctx consolectx.Context) gin.HandlerFunc { return func(c *gin.Context) { req := &model.ServiceGraphReq{} From 9adece096497b5cb8ee98329db4035c47a9bf6e9 Mon Sep 17 00:00:00 2001 From: Comrade Yi Date: Sun, 8 Mar 2026 15:52:40 +0800 Subject: [PATCH 5/5] feat(service): add service detail and interfaces endpoints with request/response models --- pkg/console/handler/service.go | 38 +++ pkg/console/model/graph.go | 15 +- pkg/console/model/service.go | 33 ++ pkg/console/router/router.go | 4 +- pkg/console/service/service.go | 292 ++++++++++-------- .../store/index/service_consumer_metadata.go | 14 + .../store/index/service_provider_metadata.go | 14 + 7 files changed, 285 insertions(+), 125 deletions(-) diff --git a/pkg/console/handler/service.go b/pkg/console/handler/service.go index 19d1e4028..a6e6a3865 100644 --- a/pkg/console/handler/service.go +++ b/pkg/console/handler/service.go @@ -248,3 +248,41 @@ func GetServiceGraph(ctx consolectx.Context) gin.HandlerFunc { c.JSON(http.StatusOK, model.NewSuccessResp(resp)) } } + +// GetServiceDetail returns service detail information +func GetServiceDetail(ctx consolectx.Context) gin.HandlerFunc { + return func(c *gin.Context) { + req := &model.ServiceDetailReq{} + if err := c.ShouldBindQuery(req); err != nil { + c.JSON(http.StatusBadRequest, model.NewErrorResp(err.Error())) + return + } + + resp, err := service.GetServiceDetail(ctx, req) + if err != nil { + util.HandleServiceError(c, err) + return + } + + c.JSON(http.StatusOK, model.NewSuccessResp(resp)) + } +} + +// GetServiceInterfaces returns service interfaces information +func GetServiceInterfaces(ctx consolectx.Context) gin.HandlerFunc { + return func(c *gin.Context) { + req := &model.ServiceInterfacesReq{} + if err := c.ShouldBindQuery(req); err != nil { + c.JSON(http.StatusBadRequest, model.NewErrorResp(err.Error())) + return + } + + resp, err := service.GetServiceInterfaces(ctx, req) + if err != nil { + util.HandleServiceError(c, err) + return + } + + c.JSON(http.StatusOK, model.NewSuccessResp(resp)) + } +} diff --git a/pkg/console/model/graph.go b/pkg/console/model/graph.go index 67bc913ea..333b9a07b 100644 --- a/pkg/console/model/graph.go +++ b/pkg/console/model/graph.go @@ -19,13 +19,17 @@ package model import ( meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1" + + "github.com/apache/dubbo-admin/pkg/common/constants" ) // GraphNode represents a node in the graph for AntV G6 type GraphNode struct { ID string `json:"id"` Label string `json:"label"` - Data interface{} `json:"data,omitempty"` // Additional data for the node + Type string `json:"type"` // "application" or "service" + Rule string `json:"rule"` // "provider", "consumer", or "" + Data interface{} `json:"data,omitempty"` } // GraphEdge represents an edge in the graph for AntV G6 @@ -57,6 +61,13 @@ type CrossLinkedListGraph struct { // ServiceGraphReq represents the request parameters for fetching the service graph type ServiceGraphReq struct { - ServiceName string `json:"serviceName" form:"serviceName"` Mesh string `json:"mesh" form:"mesh" binding:"required"` + ServiceName string `json:"serviceName" form:"serviceName" binding:"required"` + Version string `json:"version" form:"version"` + Group string `json:"group" form:"group"` +} + +// ServiceKey returns the unique service identifier +func (s *ServiceGraphReq) ServiceKey() string { + return s.ServiceName + constants.ColonSeparator + s.Version + constants.ColonSeparator + s.Group } diff --git a/pkg/console/model/service.go b/pkg/console/model/service.go index 83feea3d2..3c6694728 100644 --- a/pkg/console/model/service.go +++ b/pkg/console/model/service.go @@ -120,3 +120,36 @@ func (s *BaseServiceReq) Query(c *gin.Context) error { func (s *BaseServiceReq) ServiceKey() string { return s.ServiceName + constants.ColonSeparator + s.Version + constants.ColonSeparator + s.Group } + +type ServiceDetailReq struct { + ServiceName string `form:"serviceName" json:"serviceName" binding:"required"` + Version string `form:"version" json:"version"` + Group string `form:"group" json:"group"` + Mesh string `form:"mesh" json:"mesh" binding:"required"` +} + +type VersionGroup struct { + Version string `json:"version"` + Group string `json:"group"` +} + +type ServiceDetailResp struct { + VersionGroups []*VersionGroup `json:"versionGroups"` + AvgRT string `json:"avgRT"` + AvgQPS string `json:"avgQPS"` + RequestTotal string `json:"requestTotal"` +} + +type ServiceInterfacesReq struct { + ServiceName string `form:"serviceName" json:"serviceName" binding:"required"` + Mesh string `form:"mesh" json:"mesh" binding:"required"` +} + +type ServiceInterface struct { + InterfaceName string `json:"interfaceName"` + MethodCount int `json:"methodCount"` +} + +type ServiceInterfacesResp struct { + Interfaces []*ServiceInterface `json:"interfaces"` +} diff --git a/pkg/console/router/router.go b/pkg/console/router/router.go index 1767cd9e2..15cff8176 100644 --- a/pkg/console/router/router.go +++ b/pkg/console/router/router.go @@ -100,8 +100,8 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) { service.GET("/distribution", handler.GetServiceTabDistribution(ctx)) service.GET("/search", handler.SearchServices(ctx)) service.GET("/graph", handler.GetServiceGraph(ctx)) - //service.GET("/detail", handler.GetServiceDetail(ctx)) - //service.GET("/interfaces", handler.GetServiceInterfaces(ctx)) + service.GET("/detail", handler.GetServiceDetail(ctx)) + service.GET("/interfaces", handler.GetServiceInterfaces(ctx)) } { diff --git a/pkg/console/service/service.go b/pkg/console/service/service.go index 3e9a13e82..b46c39675 100644 --- a/pkg/console/service/service.go +++ b/pkg/console/service/service.go @@ -18,6 +18,7 @@ package service import ( + "encoding/json" "strconv" "strings" @@ -528,168 +529,217 @@ func isArgumentRoute(condition string) bool { // If serviceName is empty, it returns all service dependencies in the mesh. // It constructs nodes (applications) and edges (dependencies) for graph visualization. func SearchServiceAsCrossLinkedList(ctx consolectx.Context, req *model.ServiceGraphReq) (*model.GraphData, error) { - // Build indexes conditionally based on whether serviceName is provided - consumerIndexes := map[string]string{ - index.ByMeshIndex: req.Mesh, - } + serviceKey := req.ServiceKey() + providerIndexes := map[string]string{ - index.ByMeshIndex: req.Mesh, + index.ByMeshIndex: req.Mesh, + index.ByServiceProviderServiceKey: serviceKey, } - // Only filter by serviceName if it's provided - if req.ServiceName != "" { - consumerIndexes[index.ByServiceConsumerServiceName] = req.ServiceName - providerIndexes[index.ByServiceProviderServiceName] = req.ServiceName + providers, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource]( + ctx.ResourceManager(), + meshresource.ServiceProviderMetadataKind, + providerIndexes) + if err != nil { + logger.Errorf("get service providers for mesh %s, serviceKey %s failed, cause: %v", req.Mesh, serviceKey, err) + return nil, bizerror.New(bizerror.InternalError, "get service providers failed, please try again") + } + + if len(providers) == 0 { + logger.Errorf("no providers found for service %s in mesh %s", serviceKey, req.Mesh) + return nil, bizerror.New(bizerror.NotFoundError, "no providers found for this service") + } + + consumerIndexes := map[string]string{ + index.ByMeshIndex: req.Mesh, + index.ByServiceConsumerServiceKey: serviceKey, } - // Use ListByIndexes instead of PageListByIndexes to get all related resources - // since we need complete dependency graph, not paginated results consumers, err := manager.ListByIndexes[*meshresource.ServiceConsumerMetadataResource]( ctx.ResourceManager(), meshresource.ServiceConsumerMetadataKind, consumerIndexes) if err != nil { - logger.Errorf("get service consumer for mesh %s failed, cause: %v", req.Mesh, err) - return nil, bizerror.New(bizerror.InternalError, "get service consumer failed, please try again") + logger.Errorf("get service consumers for mesh %s, serviceKey %s failed, cause: %v", req.Mesh, serviceKey, err) + return nil, bizerror.New(bizerror.InternalError, "get service consumers failed, please try again") } - providers, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource]( - ctx.ResourceManager(), - meshresource.ServiceProviderMetadataKind, - providerIndexes) - if err != nil { - logger.Errorf("get service provider for mesh %s failed, cause: %v", req.Mesh, err) - return nil, bizerror.New(bizerror.InternalError, "get service provider failed, please try again") - } + nodes := make([]model.GraphNode, 0) + edges := make([]model.GraphEdge, 0) - // Collect all unique applications (both consumers and providers) - consumerApps := make(map[string]bool) - for _, consumer := range consumers { - if consumer.Spec != nil { - consumerApps[consumer.Spec.ConsumerAppName] = true + // use struct{} as a zero‑size value for a lightweight set + providerAppSet := make(map[string]struct{}) + for _, provider := range providers { + if provider.Spec == nil { + continue + } + if _, ok := providerAppSet[provider.Spec.ProviderAppName]; !ok { + providerAppSet[provider.Spec.ProviderAppName] = struct{}{} + nodes = append(nodes, model.GraphNode{ + ID: provider.Spec.ProviderAppName, + Label: provider.Spec.ProviderAppName, + Type: "application", + Rule: "provider", + Data: nil, + }) } } - providerApps := make(map[string]bool) - for _, provider := range providers { - if provider.Spec != nil { - providerApps[provider.Spec.ProviderAppName] = true + nodes = append(nodes, model.GraphNode{ + ID: serviceKey, + Label: serviceKey, + Type: "service", + Rule: "", + Data: nil, + }) + + consumerAppSet := make(map[string]struct{}) + for _, consumer := range consumers { + if consumer.Spec == nil { + continue + } + if _, ok := consumerAppSet[consumer.Spec.ConsumerAppName]; !ok { + consumerAppSet[consumer.Spec.ConsumerAppName] = struct{}{} + nodes = append(nodes, model.GraphNode{ + ID: consumer.Spec.ConsumerAppName, + Label: consumer.Spec.ConsumerAppName, + Type: "application", + Rule: "consumer", + Data: nil, + }) } } - allApps := make(map[string]bool) - for app := range consumerApps { - allApps[app] = true + for providerApp := range providerAppSet { + edges = append(edges, model.GraphEdge{ + Source: serviceKey, + Target: providerApp, + Data: map[string]interface{}{ + "type": "provides", + }, + }) } - for app := range providerApps { - allApps[app] = true + + for consumerApp := range consumerAppSet { + edges = append(edges, model.GraphEdge{ + Source: consumerApp, + Target: serviceKey, + Data: map[string]interface{}{ + "type": "consumes", + }, + }) } - nodes := make([]model.GraphNode, 0, len(allApps)) - edges := make([]model.GraphEdge, 0) + return &model.GraphData{ + Nodes: nodes, + Edges: edges, + }, nil +} - // Build app to service instances mapping - // For providers: map providerAppName -> list of instances providing this service - // For consumers: map consumerAppName -> empty list (consumers don't provide instances) - appInstances := make(map[string][]*meshresource.InstanceResource) +// GetServiceDetail get service detail information including version groups and metrics +func GetServiceDetail(ctx consolectx.Context, req *model.ServiceDetailReq) (*model.ServiceDetailResp, error) { + // Query all service provider metadata resources for the given service name + indexes := map[string]string{ + index.ByServiceProviderServiceName: req.ServiceName, + } + if strutil.IsNotBlank(req.Mesh) { + indexes[index.ByMeshIndex] = req.Mesh + } - // Get all instances for the mesh first - allInstances, err := manager.ListByIndexes[*meshresource.InstanceResource]( + serviceResources, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource]( ctx.ResourceManager(), - meshresource.InstanceKind, - map[string]string{ - index.ByMeshIndex: req.Mesh, - }) + meshresource.ServiceProviderMetadataKind, + indexes, + ) + byteJsonStr, err := json.Marshal(serviceResources) + logger.Infof("service resources for service %s: %s", req.ServiceName, string(byteJsonStr)) if err != nil { - logger.Errorf("get instances for mesh %s failed, cause: %v", req.Mesh, err) + logger.Errorf("get service provider metadata for %s failed, cause: %v", req.ServiceName, err) + return nil, bizerror.New(bizerror.InternalError, "get service provider failed") } - // Build app -> instances mapping - for _, instance := range allInstances { - if instance.Spec != nil && instance.Spec.AppName != "" { - appInstances[instance.Spec.AppName] = append(appInstances[instance.Spec.AppName], instance) - } + if len(serviceResources) == 0 { + logger.Warnf("service %s not found", req.ServiceName) + return nil, bizerror.New(bizerror.NotFoundError, "service not found") } - // Build nodes for each app - // Provider nodes: data contains instances providing this service - // Consumer nodes: data is nil (consumers don't provide instances) - for appName := range allApps { - var instanceData interface{} - - instances := make([]interface{}, 0) - if appInsts, ok := appInstances[appName]; ok { - for _, instance := range appInsts { - instances = append(instances, toInstanceData(instance)) + // Collect unique version and group combinations + versionGroupMap := make(map[string]*model.VersionGroup) + for _, res := range serviceResources { + if res.Spec == nil { + continue + } + key := res.Spec.Version + "|" + res.Spec.Group + if _, exists := versionGroupMap[key]; !exists { + versionGroupMap[key] = &model.VersionGroup{ + Version: res.Spec.Version, + Group: res.Spec.Group, } } - instanceData = instances + } - nodes = append(nodes, model.GraphNode{ - ID: appName, - Label: appName, - Data: instanceData, - }) + versionGroups := make([]*model.VersionGroup, 0) + for _, vg := range versionGroupMap { + versionGroups = append(versionGroups, vg) } - edgeKeyMap := make(map[string]struct{}) - // Build edges between consumers and providers - // Only create edges between apps that actually have the service relationship - for _, consumer := range consumers { - if consumer.Spec != nil { - for _, provider := range providers { - if provider.Spec != nil { - // This is where you should check if the provider's service name and the consumer's service name match. - if consumer.Spec.ServiceName != provider.Spec.ServiceName { - continue - } - // If there are two instances, such as p1->c1 and p2->c1, two edges will be generated, which need to be merged. - // Merging logic: Only one edge is kept for identical source and target. - // However, using a loop would result in three levels of nesting. Therefore, an auxiliary map is created for efficient filtering. - edgeKey := consumer.Spec.ConsumerAppName + "->" + provider.Spec.ProviderAppName - if _, exists := edgeKeyMap[edgeKey]; exists { - continue - } - edgeKeyMap[edgeKey] = struct{}{} - edges = append(edges, model.GraphEdge{ - Source: consumer.Spec.ConsumerAppName, - Target: provider.Spec.ProviderAppName, - Data: map[string]interface{}{ - "type": "dependency", - "serviceName": req.ServiceName, - "consumerApp": consumer.Spec.ConsumerAppName, - "providerApp": provider.Spec.ProviderAppName, - }, - }) - } else { - logger.Warnf("provider spec is nil for provider resource: %s", provider.Name) - } - } - } else { - logger.Warnf("consumer spec is nil for consumer resource: %s", consumer.Name) - } + // Return service detail response with mock metrics data + // In production, metrics would come from Prometheus or a metrics store + resp := &model.ServiceDetailResp{ + VersionGroups: versionGroups, + AvgRT: "96ms", + AvgQPS: "12.5", + RequestTotal: "1386", } - return &model.GraphData{ - Nodes: nodes, - Edges: edges, - }, nil + return resp, nil } -func toInstanceData(instance *meshresource.InstanceResource) map[string]interface{} { - if instance == nil || instance.Spec == nil { - return nil +// GetServiceInterfaces get service interfaces +func GetServiceInterfaces(ctx consolectx.Context, req *model.ServiceInterfacesReq) (*model.ServiceInterfacesResp, error) { + // Query all service provider metadata resources for the given service name + indexes := map[string]string{ + index.ByServiceProviderServiceName: req.ServiceName, + } + if strutil.IsNotBlank(req.Mesh) { + indexes[index.ByMeshIndex] = req.Mesh + } + + serviceResources, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource]( + ctx.ResourceManager(), + meshresource.ServiceProviderMetadataKind, + indexes, + ) + if err != nil { + logger.Errorf("get service provider metadata for %s failed, cause: %v", req.ServiceName, err) + return nil, bizerror.New(bizerror.InternalError, "get service interfaces failed") + } + + if len(serviceResources) == 0 { + logger.Warnf("service %s not found", req.ServiceName) + return nil, bizerror.New(bizerror.NotFoundError, "service not found") + } + + // Collect all methods from service resources + methodCount := 0 + for _, res := range serviceResources { + if res.Spec == nil || res.Spec.Methods == nil { + continue + } + methodCount += len(res.Spec.Methods) + } + + // Create a single interface entry for the service with method count + interfaces := []*model.ServiceInterface{ + { + InterfaceName: req.ServiceName, + MethodCount: methodCount, + }, } - data := map[string]interface{}{ - "appName": instance.Spec.AppName, - "ip": instance.Spec.Ip, - "name": instance.Spec.Name, - "protocol": instance.Spec.Protocol, - "qosPort": instance.Spec.QosPort, - "rpcPort": instance.Spec.RpcPort, - "tags": instance.Spec.Tags, + resp := &model.ServiceInterfacesResp{ + Interfaces: interfaces, } - return data + return resp, nil } diff --git a/pkg/core/store/index/service_consumer_metadata.go b/pkg/core/store/index/service_consumer_metadata.go index 3498adc60..fe26ce6a8 100644 --- a/pkg/core/store/index/service_consumer_metadata.go +++ b/pkg/core/store/index/service_consumer_metadata.go @@ -29,12 +29,14 @@ import ( const ( ByServiceConsumerAppName = "idx_service_consumer_app_name" ByServiceConsumerServiceName = "idx_service_consumer_service_name" + ByServiceConsumerServiceKey = "idx_service_consumer_service_key" ) func init() { RegisterIndexers(meshresource.ServiceConsumerMetadataKind, map[string]cache.IndexFunc{ ByServiceConsumerAppName: byServiceConsumerAppName, ByServiceConsumerServiceName: byServiceConsumerServiceName, + ByServiceConsumerServiceKey: byServiceConsumerServiceKey, }) } @@ -59,3 +61,15 @@ func byServiceConsumerServiceName(obj interface{}) ([]string, error) { } return []string{metadata.Spec.ServiceName}, nil } + +func byServiceConsumerServiceKey(obj interface{}) ([]string, error) { + metadata, ok := obj.(*meshresource.ServiceConsumerMetadataResource) + if !ok { + return nil, bizerror.NewAssertionError(meshresource.ServiceConsumerMetadataKind, reflect.TypeOf(obj).Name()) + } + if metadata.Spec == nil { + return []string{}, nil + } + serviceKey := metadata.Spec.ServiceName + ":" + metadata.Spec.Version + ":" + metadata.Spec.Group + return []string{serviceKey}, nil +} diff --git a/pkg/core/store/index/service_provider_metadata.go b/pkg/core/store/index/service_provider_metadata.go index fe3d7fe28..6b43f4db0 100644 --- a/pkg/core/store/index/service_provider_metadata.go +++ b/pkg/core/store/index/service_provider_metadata.go @@ -29,12 +29,14 @@ import ( const ( ByServiceProviderAppName = "idx_service_provider_app_name" ByServiceProviderServiceName = "idx_service_provider_service_name" + ByServiceProviderServiceKey = "idx_service_provider_service_key" ) func init() { RegisterIndexers(meshresource.ServiceProviderMetadataKind, map[string]cache.IndexFunc{ ByServiceProviderAppName: byServiceProviderAppName, ByServiceProviderServiceName: byServiceProviderServiceName, + ByServiceProviderServiceKey: byServiceProviderServiceKey, }) } @@ -59,3 +61,15 @@ func byServiceProviderServiceName(obj interface{}) ([]string, error) { } return []string{metadata.Spec.ServiceName}, nil } + +func byServiceProviderServiceKey(obj interface{}) ([]string, error) { + metadata, ok := obj.(*meshresource.ServiceProviderMetadataResource) + if !ok { + return nil, bizerror.NewAssertionError(meshresource.ServiceProviderMetadataKind, reflect.TypeOf(obj).Name()) + } + if metadata.Spec == nil { + return []string{}, nil + } + serviceKey := metadata.Spec.ServiceName + ":" + metadata.Spec.Version + ":" + metadata.Spec.Group + return []string{serviceKey}, nil +}