Skip to content

Commit 39b4666

Browse files
Feat: Add service and application topology graph APIs based on discussion #1398
* feat(api): add GraphServices endpoint for service-level topology Based on discussion #1398, use ServiceProviderMetadata and ServiceConsumerMetadata to return provider/consumer application relations as graph nodes and edges for AntV G6. * feat(api): add GraphApplications endpoint for application-level topology Traverse provider/consumer service relations to build application-level graph. Also add idx_service_consumer_service_key index to support efficient serviceKey queries. * feat(api): add graph models (GraphNode, GraphEdge, GraphData) in pkg/console/model/graph.go * feat(api): add ApplicationGraphReq, ServiceGraphReq and GetApplicationGraph, GetServiceGraph handlers * feat(router): register /application/graph and /service/graph routes * feat(api): fix error handling to use direct err pass-through instead of MeshNotFoundError
1 parent c77a34a commit 39b4666

8 files changed

Lines changed: 398 additions & 2 deletions

File tree

pkg/console/handler/application.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,23 @@ func ApplicationSearch(ctx consolectx.Context) gin.HandlerFunc {
9898
}
9999
}
100100

101+
func GetApplicationGraph(ctx consolectx.Context) gin.HandlerFunc {
102+
return func(c *gin.Context) {
103+
req := model.NewApplicationGraphReq()
104+
if err := c.ShouldBindQuery(req); err != nil {
105+
util.HandleArgumentError(c, err)
106+
return
107+
}
108+
109+
resp, err := service.GraphApplications(ctx, req)
110+
if err != nil {
111+
util.HandleServiceError(c, err)
112+
return
113+
}
114+
c.JSON(http.StatusOK, model.NewSuccessResp(resp))
115+
}
116+
}
117+
101118
func ApplicationConfigAccessLogPut(ctx consolectx.Context) gin.HandlerFunc {
102119
return func(c *gin.Context) {
103120
appName := c.Query("appName")

pkg/console/handler/service.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,22 @@ func ServiceConfigArgumentRoutePUT(ctx consolectx.Context) gin.HandlerFunc {
306306
c.JSON(http.StatusOK, model.NewSuccessResp(nil))
307307
}
308308
}
309+
310+
// GetServiceGraph returns the service graph as graph data (nodes and edges) for visualization
311+
func GetServiceGraph(ctx consolectx.Context) gin.HandlerFunc {
312+
return func(c *gin.Context) {
313+
req := &model.ServiceGraphReq{}
314+
if err := c.ShouldBindQuery(req); err != nil {
315+
c.JSON(http.StatusBadRequest, model.NewErrorResp(err.Error()))
316+
return
317+
}
318+
319+
resp, err := service.GraphServices(ctx, req)
320+
if err != nil {
321+
util.HandleServiceError(c, err)
322+
return
323+
}
324+
325+
c.JSON(http.StatusOK, model.NewSuccessResp(resp))
326+
}
327+
}

pkg/console/model/application.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,23 @@ func NewApplicationSearchReq() *ApplicationSearchReq {
189189
}
190190
}
191191

192+
type ApplicationGraphReq struct {
193+
coremodel.PageReq
194+
195+
AppName string `form:"appName" json:"appName"`
196+
Keywords string `form:"keywords" json:"keywords"`
197+
Mesh string `form:"mesh" json:"mesh"`
198+
}
199+
200+
func NewApplicationGraphReq() *ApplicationGraphReq {
201+
return &ApplicationGraphReq{
202+
PageReq: coremodel.PageReq{
203+
PageOffset: 0,
204+
PageSize: 15,
205+
},
206+
}
207+
}
208+
192209
type ApplicationSearchResp struct {
193210
AppName string `json:"appName"`
194211
DeployClusters []string `json:"deployClusters"`

pkg/console/model/graph.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package model
19+
20+
import (
21+
meshresource "github.com/apache/dubbo-admin/pkg/core/resource/apis/mesh/v1alpha1"
22+
23+
"github.com/apache/dubbo-admin/pkg/common/constants"
24+
)
25+
26+
// GraphNode represents a node in the graph for AntV G6
27+
type GraphNode struct {
28+
ID string `json:"id"`
29+
Label string `json:"label"`
30+
Type string `json:"type"` // "application" or "service"
31+
Rule string `json:"rule"` // "provider", "consumer", or ""
32+
Data interface{} `json:"data,omitempty"`
33+
}
34+
35+
// GraphEdge represents an edge in the graph for AntV G6
36+
type GraphEdge struct {
37+
Source string `json:"source"`
38+
Target string `json:"target"`
39+
Data map[string]interface{} `json:"data,omitempty"` // Additional data for the edge
40+
}
41+
42+
// GraphData represents the complete graph structure for AntV G6
43+
type GraphData struct {
44+
Nodes []GraphNode `json:"nodes"`
45+
Edges []GraphEdge `json:"edges"`
46+
}
47+
48+
// CrossNode represents a node in the cross-linked list structure
49+
type CrossNode struct {
50+
Instance *meshresource.InstanceResource
51+
Next *CrossNode // pointer to next node in the same row
52+
Down *CrossNode // pointer to next node in the same column
53+
}
54+
55+
// CrossLinkedListGraph represents the cross-linked list structure as a directed graph
56+
type CrossLinkedListGraph struct {
57+
Head *CrossNode
58+
Rows int // number of rows
59+
Cols int // number of columns
60+
}
61+
62+
// ServiceGraphReq represents the request parameters for fetching the service graph
63+
type ServiceGraphReq struct {
64+
Mesh string `json:"mesh" form:"mesh" binding:"required"`
65+
ServiceName string `json:"serviceName" form:"serviceName" binding:"required"`
66+
Version string `json:"version" form:"version"`
67+
Group string `json:"group" form:"group"`
68+
}
69+
70+
// ServiceKey returns the unique service identifier
71+
func (s *ServiceGraphReq) ServiceKey() string {
72+
return s.ServiceName + constants.ColonSeparator + s.Version + constants.ColonSeparator + s.Group
73+
}

pkg/console/router/router.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) {
6060
application.GET("/instance/info", handler.GetApplicationTabInstanceInfo(ctx))
6161
application.GET("/service/form", handler.GetApplicationServiceForm(ctx))
6262
application.GET("/search", handler.ApplicationSearch(ctx))
63+
application.GET("/graph", handler.GetApplicationGraph(ctx))
6364
{
6465
applicationConfig := application.Group("/config")
6566
applicationConfig.PUT("/operatorLog", handler.ApplicationConfigAccessLogPut(ctx))
@@ -103,8 +104,7 @@ func InitRouter(r *gin.Engine, ctx consolectx.Context) {
103104
service.GET("/provider-instances", handler.GetServiceProviderInstances(ctx))
104105
service.GET("/methods", handler.GetServiceMethodNames(ctx))
105106
service.GET("/search", handler.SearchServices(ctx))
106-
//service.GET("/detail", handler.GetServiceDetail(ctx))
107-
//service.GET("/interfaces", handler.GetServiceInterfaces(ctx))
107+
service.GET("/graph", handler.GetServiceGraph(ctx))
108108
}
109109

110110
{

pkg/console/service/application.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,140 @@ func getAppConsumeServiceInfo(ctx consolectx.Context, req *model.ApplicationServ
202202
return pageResult, nil
203203
}
204204

205+
// GraphApplications returns the application-level graph for a given application.
206+
// It collects provider and consumer service relations and transforms them into nodes and edges.
207+
// The current implementation is a simplified version (provider/consumer link traversal).
208+
func GraphApplications(ctx consolectx.Context, req *model.ApplicationGraphReq) (*model.GraphData, error) {
209+
210+
// Step 1: query all services provided by this application in the namespace.
211+
providerServiceList, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource](
212+
ctx.ResourceManager(),
213+
meshresource.ServiceProviderMetadataKind,
214+
[]index.IndexCondition{{IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals},
215+
{IndexName: index.ByServiceProviderAppName, Value: req.AppName, Operator: index.Equals},
216+
},
217+
)
218+
if err != nil {
219+
// manager.ListByIndexes 内部已经有合适的 error 返回,直接透传即可
220+
return nil, err
221+
}
222+
223+
// Step 2: query all services consumed by this application in the namespace.
224+
consumerServiceList, err := manager.ListByIndexes[*meshresource.ServiceConsumerMetadataResource](
225+
ctx.ResourceManager(),
226+
meshresource.ServiceConsumerMetadataKind,
227+
[]index.IndexCondition{{IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals},
228+
{IndexName: index.ByServiceConsumerAppName, Value: req.AppName, Operator: index.Equals},
229+
},
230+
)
231+
if err != nil {
232+
return nil, err
233+
}
234+
235+
// Step 3: build the graph nodes and edges from provider and consumer relations.
236+
// providerAppSet and consumerAppSet track already-added application nodes.
237+
providerAppSet := make(map[string]struct{})
238+
consumerAppSet := make(map[string]struct{})
239+
240+
nodes := make([]model.GraphNode, 0)
241+
edges := make([]model.GraphEdge, 0)
242+
// init self node
243+
nodes = append(nodes, model.GraphNode{
244+
ID: req.AppName,
245+
Label: req.AppName,
246+
Type: "application",
247+
Rule: "", // self node doesn't have a rule
248+
Data: nil,
249+
})
250+
// 3.a: iterate over provided services, collect service nodes and consumer app nodes.
251+
for _, provider := range providerServiceList {
252+
if provider.Spec == nil {
253+
continue
254+
}
255+
256+
// For each provided service, find consuming applications and add them as nodes.
257+
consumerAppServiceList, err := manager.ListByIndexes[*meshresource.ServiceConsumerMetadataResource](
258+
ctx.ResourceManager(),
259+
meshresource.ServiceConsumerMetadataKind,
260+
[]index.IndexCondition{{IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals},
261+
{IndexName: index.ByServiceConsumerServiceKey, Value: provider.Spec.ServiceName + ":" + provider.Spec.Version + ":" + provider.Spec.Group, Operator: index.Equals},
262+
},
263+
)
264+
if err != nil {
265+
logger.Errorf("failed to list consumer apps by provider service key, mesh: %s, serviceKey: %s, err: %s", req.Mesh, provider.Spec.ProviderAppName+":"+provider.Spec.Version+":"+provider.Spec.Group, err)
266+
continue
267+
}
268+
269+
for _, item := range consumerAppServiceList {
270+
if item.Spec == nil {
271+
continue
272+
}
273+
if _, ok := consumerAppSet[item.Spec.ConsumerAppName]; !ok {
274+
consumerAppSet[item.Spec.ConsumerAppName] = struct{}{}
275+
nodes = append(nodes, model.GraphNode{
276+
ID: item.Spec.ConsumerAppName,
277+
Label: item.Spec.ConsumerAppName,
278+
Type: "application",
279+
Rule: constants.ConsumerSide,
280+
Data: nil,
281+
})
282+
edges = append(edges, model.GraphEdge{
283+
Source: item.Spec.ConsumerAppName,
284+
Target: provider.Spec.ProviderAppName,
285+
Data: nil,
286+
})
287+
}
288+
}
289+
}
290+
291+
// 3.b: iterate over consumed services, collect service nodes and provider app nodes.
292+
for _, consumer := range consumerServiceList {
293+
if consumer.Spec == nil {
294+
continue
295+
}
296+
297+
// For each consumed service, find providing applications and add them as nodes.
298+
providerAppList, err := manager.ListByIndexes[*meshresource.ServiceProviderMetadataResource](
299+
ctx.ResourceManager(),
300+
meshresource.ServiceProviderMetadataKind,
301+
[]index.IndexCondition{{IndexName: index.ByMeshIndex, Value: req.Mesh, Operator: index.Equals},
302+
{IndexName: index.ByServiceProviderServiceKey, Value: consumer.Spec.ServiceName + ":" + consumer.Spec.Version + ":" + consumer.Spec.Group, Operator: index.Equals},
303+
},
304+
)
305+
if err != nil {
306+
logger.Errorf("failed to list consumer apps by provider service key, mesh: %s, serviceKey: %s, err: %s", req.Mesh, consumer.Spec.ConsumerAppName+":"+consumer.Spec.Version+":"+consumer.Spec.Group, err)
307+
continue
308+
}
309+
310+
for _, item := range providerAppList {
311+
if item.Spec == nil {
312+
continue
313+
}
314+
if _, ok := providerAppSet[item.Spec.ProviderAppName]; !ok {
315+
providerAppSet[item.Spec.ProviderAppName] = struct{}{}
316+
nodes = append(nodes, model.GraphNode{
317+
ID: item.Spec.ProviderAppName,
318+
Label: item.Spec.ProviderAppName,
319+
Type: "application",
320+
Rule: constants.ProviderSide,
321+
Data: nil,
322+
})
323+
edges = append(edges, model.GraphEdge{
324+
Source: consumer.Spec.ConsumerAppName,
325+
Target: item.Spec.ProviderAppName,
326+
Data: nil,
327+
})
328+
}
329+
}
330+
}
331+
332+
// Step 4: assemble and return graph data (nodes + edges).
333+
return &model.GraphData{
334+
Nodes: nodes,
335+
Edges: edges,
336+
}, nil
337+
}
338+
205339
func SearchApplications(ctx consolectx.Context, req *model.ApplicationSearchReq) (*model.SearchPaginationResult, error) {
206340
if strutil.IsNotBlank(req.Keywords) {
207341
appResList, err := SearchApplicationsByKeywords(ctx, &model.SearchReq{

0 commit comments

Comments
 (0)