You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2020/03/06 10:01:10 UTC

[skywalking-cli] 01/01: [Feature] Support multiple linear metrics and enhance Ascii Graph Display

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch feature/linear-metrics
in repository https://gitbox.apache.org/repos/asf/skywalking-cli.git

commit e697a78653eb8b2d83ffdb015839de9662e0ad03
Author: kezhenxu94 <ke...@163.com>
AuthorDate: Fri Mar 6 18:00:53 2020 +0800

    [Feature] Support multiple linear metrics and enhance Ascii Graph Display
    
    ### Motivation
    
    Adopt the new API of multiple linear metrics, https://github.com/apache/skywalking/pull/4214
    
    ### Modification
    
    - Rewrite the pxx metrics with the new API
    
    - Slightly refactor the GraphQL client into individual package, according to the query protocol
    
    - Enhance the Ascii Graph display style
    
    ### Result
    
    - CLI is compatible with the latest backend
    
    - Ascii Graph can display multiple charts in one screen
---
 .golangci.yml                                      |   2 +-
 commands/endpoint/list.go                          |   5 +-
 commands/instance/instance.go                      |   5 +-
 commands/instance/list.go                          |   5 +-
 commands/instance/search.go                        |   5 +-
 commands/interceptor/duration.go                   |  12 +-
 commands/metrics/linear/linear-metrics.go          |  51 +++++--
 commands/metrics/single/single-metrics.go          |   7 +-
 commands/service/list.go                           |   7 +-
 display/display.go                                 |   8 +-
 display/graph/graph.go                             |  14 +-
 display/graph/linear/linear.go                     |  82 +++++++----
 graphql/client/client.go                           | 150 +--------------------
 graphql/metadata/metadata.go                       |  93 ++++++++++++-
 graphql/metrics/metrics.go                         |  64 +++++++++
 graphql/{schema => utils}/constants.go             |  32 +++--
 display/graph/graph.go => graphql/utils/strings.go |  22 +--
 17 files changed, 329 insertions(+), 235 deletions(-)

diff --git a/.golangci.yml b/.golangci.yml
index cebf66b..26b94e2 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -38,7 +38,7 @@ linters-settings:
   misspell:
     locale: US
   lll:
-    line-length: 120
+    line-length: 150
   goimports:
     local-prefixes: github.com/apache/skywalking-cli
   gocritic:
diff --git a/commands/endpoint/list.go b/commands/endpoint/list.go
index 51ea320..034c6a5 100644
--- a/commands/endpoint/list.go
+++ b/commands/endpoint/list.go
@@ -20,8 +20,9 @@ package endpoint
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/graphql/metadata"
+
 	"github.com/apache/skywalking-cli/display"
-	"github.com/apache/skywalking-cli/graphql/client"
 )
 
 var ListCommand = cli.Command{
@@ -53,7 +54,7 @@ var ListCommand = cli.Command{
 		limit := ctx.Int("limit")
 		keyword := ctx.String("keyword")
 
-		endpoints := client.SearchEndpoints(ctx, serviceID, keyword, limit)
+		endpoints := metadata.SearchEndpoints(ctx, serviceID, keyword, limit)
 
 		return display.Display(ctx, endpoints)
 	},
diff --git a/commands/instance/instance.go b/commands/instance/instance.go
index da83c80..29c86a7 100644
--- a/commands/instance/instance.go
+++ b/commands/instance/instance.go
@@ -20,7 +20,8 @@ package instance
 import (
 	"github.com/urfave/cli"
 
-	"github.com/apache/skywalking-cli/graphql/client"
+	"github.com/apache/skywalking-cli/graphql/metadata"
+
 	"github.com/apache/skywalking-cli/logger"
 )
 
@@ -43,7 +44,7 @@ func verifyAndSwitchServiceParameter(ctx *cli.Context) string {
 	}
 
 	if serviceID == "" && serviceName != "" {
-		service, err := client.SearchService(ctx, serviceName)
+		service, err := metadata.SearchService(ctx, serviceName)
 		if err != nil {
 			logger.Log.Fatalln(err)
 		}
diff --git a/commands/instance/list.go b/commands/instance/list.go
index c20ccb4..eda72c1 100644
--- a/commands/instance/list.go
+++ b/commands/instance/list.go
@@ -20,11 +20,12 @@ package instance
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/graphql/metadata"
+
 	"github.com/apache/skywalking-cli/commands/flags"
 	"github.com/apache/skywalking-cli/commands/interceptor"
 	"github.com/apache/skywalking-cli/commands/model"
 	"github.com/apache/skywalking-cli/display"
-	"github.com/apache/skywalking-cli/graphql/client"
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -44,7 +45,7 @@ var ListCommand = cli.Command{
 		start := ctx.String("start")
 		step := ctx.Generic("step")
 
-		instances := client.Instances(ctx, serviceID, schema.Duration{
+		instances := metadata.Instances(ctx, serviceID, schema.Duration{
 			Start: start,
 			End:   end,
 			Step:  step.(*model.StepEnumValue).Selected,
diff --git a/commands/instance/search.go b/commands/instance/search.go
index b9e2ac9..b97adae 100644
--- a/commands/instance/search.go
+++ b/commands/instance/search.go
@@ -20,13 +20,14 @@ package instance
 import (
 	"regexp"
 
+	"github.com/apache/skywalking-cli/graphql/metadata"
+
 	"github.com/urfave/cli"
 
 	"github.com/apache/skywalking-cli/commands/flags"
 	"github.com/apache/skywalking-cli/commands/interceptor"
 	"github.com/apache/skywalking-cli/commands/model"
 	"github.com/apache/skywalking-cli/display"
-	"github.com/apache/skywalking-cli/graphql/client"
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -47,7 +48,7 @@ var SearchCommand = cli.Command{
 
 		regex := ctx.String("regex")
 
-		instances := client.Instances(ctx, serviceID, schema.Duration{
+		instances := metadata.Instances(ctx, serviceID, schema.Duration{
 			Start: start,
 			End:   end,
 			Step:  step.(*model.StepEnumValue).Selected,
diff --git a/commands/interceptor/duration.go b/commands/interceptor/duration.go
index 3eb355a..c52ae69 100644
--- a/commands/interceptor/duration.go
+++ b/commands/interceptor/duration.go
@@ -21,6 +21,8 @@ import (
 	"strconv"
 	"time"
 
+	"github.com/apache/skywalking-cli/graphql/utils"
+
 	"github.com/urfave/cli"
 
 	"github.com/apache/skywalking-cli/graphql/schema"
@@ -29,7 +31,7 @@ import (
 
 func tryParseTime(unparsed string) (schema.Step, time.Time, error) {
 	var possibleError error = nil
-	for step, layout := range schema.StepFormats {
+	for step, layout := range utils.StepFormats {
 		t, err := time.Parse(layout, unparsed)
 		if err == nil {
 			return step, t, nil
@@ -48,9 +50,9 @@ func DurationInterceptor(ctx *cli.Context) error {
 
 	startTime, endTime, step := ParseDuration(start, end, timezone)
 
-	if err := ctx.Set("start", startTime.Format(schema.StepFormats[step])); err != nil {
+	if err := ctx.Set("start", startTime.Format(utils.StepFormats[step])); err != nil {
 		return err
-	} else if err := ctx.Set("end", endTime.Format(schema.StepFormats[step])); err != nil {
+	} else if err := ctx.Set("end", endTime.Format(utils.StepFormats[step])); err != nil {
 		return err
 	} else if err := ctx.Set("step", step.String()); err != nil {
 		return err
@@ -103,12 +105,12 @@ func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, s
 		if step, startTime, err = tryParseTime(start); err != nil {
 			logger.Log.Fatalln("Unsupported time format:", start, err)
 		}
-		return startTime, startTime.Add(30 * schema.StepDuration[step]), step
+		return startTime, startTime.Add(30 * utils.StepDuration[step]), step
 	} else { // start is absent
 		if step, endTime, err = tryParseTime(end); err != nil {
 			logger.Log.Fatalln("Unsupported time format:", end, err)
 		}
-		return endTime.Add(-30 * schema.StepDuration[step]), endTime, step
+		return endTime.Add(-30 * utils.StepDuration[step]), endTime, step
 	}
 }
 
diff --git a/commands/metrics/linear/linear-metrics.go b/commands/metrics/linear/linear-metrics.go
index ceda447..ba88527 100644
--- a/commands/metrics/linear/linear-metrics.go
+++ b/commands/metrics/linear/linear-metrics.go
@@ -18,13 +18,19 @@
 package linear
 
 import (
+	"time"
+
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/graphql/utils"
+	"github.com/apache/skywalking-cli/logger"
+
+	"github.com/apache/skywalking-cli/graphql/metrics"
+
 	"github.com/apache/skywalking-cli/commands/flags"
 	"github.com/apache/skywalking-cli/commands/interceptor"
 	"github.com/apache/skywalking-cli/commands/model"
 	"github.com/apache/skywalking-cli/display"
-	"github.com/apache/skywalking-cli/graphql/client"
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -36,7 +42,7 @@ var Command = cli.Command{
 		[]cli.Flag{
 			cli.StringFlag{
 				Name:     "name",
-				Usage:    "metrics `NAME`, such as `all_p99`",
+				Usage:    "metrics `NAME`, such as `all_percentile`",
 				Required: true,
 			},
 			cli.StringFlag{
@@ -44,6 +50,12 @@ var Command = cli.Command{
 				Usage:    "`ID`, the related id if the metrics requires one",
 				Required: false,
 			},
+			cli.IntFlag{
+				Name:     "num",
+				Usage:    "`num`, the number of linear metrics to query, (default: 5)",
+				Required: false,
+				Value:    5,
+			},
 		},
 	),
 	Before: interceptor.BeforeChain([]cli.BeforeFunc{
@@ -55,6 +67,7 @@ var Command = cli.Command{
 		start := ctx.String("start")
 		step := ctx.Generic("step")
 		metricsName := ctx.String("name")
+		numOfLinear := ctx.Int("num")
 
 		var id *string = nil
 
@@ -62,15 +75,37 @@ var Command = cli.Command{
 			id = &idString
 		}
 
-		metricsValues := client.LinearIntValues(ctx, schema.MetricCondition{
-			Name: metricsName,
-			ID:   id,
-		}, schema.Duration{
+		duration := schema.Duration{
 			Start: start,
 			End:   end,
 			Step:  step.(*model.StepEnumValue).Selected,
-		})
+		}
 
-		return display.Display(ctx, metricsValues)
+		values := metrics.MultipleLinearIntValues(ctx, schema.MetricCondition{
+			Name: metricsName,
+			ID:   id,
+		}, numOfLinear, duration)
+
+		reshaped := make([]map[string]float64, len(values))
+
+		for index, value := range values {
+			reshaped[index] = metricsToMap(duration, value.Values)
+		}
+
+		return display.Display(ctx, reshaped)
 	},
 }
+
+func metricsToMap(duration schema.Duration, kvInts []*schema.KVInt) map[string]float64 {
+	values := map[string]float64{}
+	format := utils.StepFormats[duration.Step]
+	startTime, err := time.Parse(format, duration.Start)
+	if err != nil {
+		logger.Log.Fatalln(err)
+	}
+	step := utils.StepDuration[duration.Step]
+	for idx, value := range kvInts {
+		values[startTime.Add(time.Duration(idx)*step).Format(format)] = float64(value.Value)
+	}
+	return values
+}
diff --git a/commands/metrics/single/single-metrics.go b/commands/metrics/single/single-metrics.go
index 99817da..3f04d92 100644
--- a/commands/metrics/single/single-metrics.go
+++ b/commands/metrics/single/single-metrics.go
@@ -20,13 +20,14 @@ package single
 import (
 	"strings"
 
+	"github.com/apache/skywalking-cli/graphql/metrics"
+
 	"github.com/urfave/cli"
 
 	"github.com/apache/skywalking-cli/commands/flags"
 	"github.com/apache/skywalking-cli/commands/interceptor"
 	"github.com/apache/skywalking-cli/commands/model"
 	"github.com/apache/skywalking-cli/display"
-	"github.com/apache/skywalking-cli/graphql/client"
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -65,7 +66,7 @@ var Command = cli.Command{
 			ids = append(ids, strings.Split(id, ",")...)
 		}
 
-		metricsValues := client.IntValues(ctx, schema.BatchMetricConditions{
+		metricsValues := metrics.IntValues(ctx, schema.BatchMetricConditions{
 			Name: metricsName,
 			Ids:  ids,
 		}, schema.Duration{
@@ -74,6 +75,6 @@ var Command = cli.Command{
 			Step:  step.(*model.StepEnumValue).Selected,
 		})
 
-		return display.Display(ctx, metricsValues)
+		return display.Display(ctx, metricsValues.Values)
 	},
 }
diff --git a/commands/service/list.go b/commands/service/list.go
index 8400a47..2604a6f 100644
--- a/commands/service/list.go
+++ b/commands/service/list.go
@@ -20,11 +20,12 @@ package service
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/graphql/metadata"
+
 	"github.com/apache/skywalking-cli/commands/flags"
 	"github.com/apache/skywalking-cli/commands/interceptor"
 	"github.com/apache/skywalking-cli/commands/model"
 	"github.com/apache/skywalking-cli/display"
-	"github.com/apache/skywalking-cli/graphql/client"
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -47,13 +48,13 @@ var ListCommand = cli.Command{
 		var services []schema.Service
 
 		if args := ctx.Args(); len(args) == 0 {
-			services = client.Services(ctx, schema.Duration{
+			services = metadata.AllServices(ctx, schema.Duration{
 				Start: start,
 				End:   end,
 				Step:  step.(*model.StepEnumValue).Selected,
 			})
 		} else {
-			service, _ := client.SearchService(ctx, args.First())
+			service, _ := metadata.SearchService(ctx, args.First())
 			services = []schema.Service{service}
 		}
 
diff --git a/display/display.go b/display/display.go
index 48be6ab..e620673 100644
--- a/display/display.go
+++ b/display/display.go
@@ -31,10 +31,10 @@ import (
 )
 
 const (
-	JSON  string = "json"
-	YAML  string = "yaml"
-	TABLE string = "table"
-	GRAPH string = "graph"
+	JSON  = "json"
+	YAML  = "yaml"
+	TABLE = "table"
+	GRAPH = "graph"
 )
 
 // Display the object in the style specified in flag --display
diff --git a/display/graph/graph.go b/display/graph/graph.go
index 7184360..c27f835 100644
--- a/display/graph/graph.go
+++ b/display/graph/graph.go
@@ -25,11 +25,17 @@ import (
 )
 
 func Display(object interface{}) error {
-	if reflect.TypeOf(object) != reflect.TypeOf(map[string]float64{}) {
-		return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(object))
+	if reflect.TypeOf(object) == reflect.TypeOf(map[string]float64{}) {
+		kvs := []map[string]float64{object.(map[string]float64)}
+
+		return linear.Display(kvs)
 	}
 
-	kvs := object.(map[string]float64)
+	if reflect.TypeOf(object) == reflect.TypeOf([]map[string]float64{}) {
+		kvs := object.([]map[string]float64)
+
+		return linear.Display(kvs)
+	}
 
-	return linear.Display(kvs)
+	return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(object))
 }
diff --git a/display/graph/linear/linear.go b/display/graph/linear/linear.go
index b6e6d07..c632112 100644
--- a/display/graph/linear/linear.go
+++ b/display/graph/linear/linear.go
@@ -19,14 +19,15 @@ package linear
 
 import (
 	"context"
+	"fmt"
+	"math"
 	"strings"
 
-	"github.com/mum4k/termdash/widgetapi"
+	"github.com/mum4k/termdash/linestyle"
 
 	"github.com/mum4k/termdash"
 	"github.com/mum4k/termdash/container"
 	"github.com/mum4k/termdash/container/grid"
-	"github.com/mum4k/termdash/linestyle"
 	"github.com/mum4k/termdash/terminal/termbox"
 	"github.com/mum4k/termdash/terminal/terminalapi"
 	"github.com/mum4k/termdash/widgets/linechart"
@@ -34,20 +35,19 @@ import (
 
 const RootID = "root"
 
-func newWidgets(inputs map[string]float64) (lineChart *linechart.LineChart, err error) {
+func newLineChart(inputs map[string]float64) (lineChart *linechart.LineChart, err error) {
 	index := 0
 
 	xLabels := map[int]string{}
-	var yValues []float64
+	yValues := make([]float64, len(inputs))
+
 	for xLabel, yValue := range inputs {
 		xLabels[index] = xLabel
+		yValues[index] = yValue
 		index++
-		yValues = append(yValues, yValue)
 	}
 
-	if lineChart, err = linechart.New(
-		linechart.YAxisAdaptive(),
-	); err != nil {
+	if lineChart, err = linechart.New(linechart.YAxisAdaptive()); err != nil {
 		return
 	}
 
@@ -56,21 +56,42 @@ func newWidgets(inputs map[string]float64) (lineChart *linechart.LineChart, err
 	return lineChart, err
 }
 
-func gridLayout(lineChart widgetapi.Widget) ([]container.Option, error) {
-	widget := grid.Widget(
-		lineChart,
-		container.Border(linestyle.Light),
-		container.BorderTitleAlignCenter(),
-		container.BorderTitle("Press q to quit"),
-	)
+func layout(lineCharts ...*linechart.LineChart) ([]container.Option, error) {
+	cols := maxSqrt(len(lineCharts))
+
+	rows := make([][]grid.Element, int(math.Ceil(float64(len(lineCharts))/float64(cols))))
+
+	for r := 0; r < len(rows); r++ {
+		var row []grid.Element
+		for c := 0; c < cols && r*cols+c < len(lineCharts); c++ {
+			percentage := int(math.Floor(float64(100) / float64(cols)))
+			if r == len(rows)-1 {
+				percentage = int(math.Floor(float64(100) / float64(len(lineCharts)-r*cols)))
+			}
+			row = append(row, grid.ColWidthPerc(
+				int(math.Min(99, float64(percentage))),
+				grid.Widget(
+					lineCharts[r*cols+c],
+					container.Border(linestyle.Light),
+					container.BorderTitleAlignCenter(),
+					container.BorderTitle(fmt.Sprintf("#%v", r*cols+c)),
+				),
+			))
+		}
+		rows[r] = row
+	}
 
 	builder := grid.New()
-	builder.Add(widget)
+
+	for _, row := range rows {
+		percentage := int(math.Min(99, float64(100/len(rows))))
+		builder.Add(grid.RowHeightPerc(percentage, row...))
+	}
 
 	return builder.Build()
 }
 
-func Display(inputs map[string]float64) error {
+func Display(inputs []map[string]float64) error {
 	t, err := termbox.New()
 	if err != nil {
 		return err
@@ -80,26 +101,31 @@ func Display(inputs map[string]float64) error {
 	c, err := container.New(
 		t,
 		container.ID(RootID),
-		container.PaddingTop(2),
-		container.PaddingRight(2),
-		container.PaddingBottom(2),
-		container.PaddingLeft(2),
 	)
 	if err != nil {
 		return err
 	}
 
-	w, err := newWidgets(inputs)
-	if err != nil {
-		return err
+	var elements []*linechart.LineChart
+
+	for _, input := range inputs {
+		w, e := newLineChart(input)
+		if e != nil {
+			return e
+		}
+		elements = append(elements, w)
 	}
 
-	gridOpts, err := gridLayout(w)
+	gridOpts, err := layout(elements...)
 	if err != nil {
 		return err
 	}
 
-	err = c.Update(RootID, gridOpts...)
+	err = c.Update(RootID, append(
+		gridOpts,
+		container.Border(linestyle.Light),
+		container.BorderTitle("PRESS Q TO QUIT"))...,
+	)
 
 	if err != nil {
 		return err
@@ -116,3 +142,7 @@ func Display(inputs map[string]float64) error {
 
 	return err
 }
+
+func maxSqrt(num int) int {
+	return int(math.Ceil(math.Sqrt(float64(num))))
+}
diff --git a/graphql/client/client.go b/graphql/client/client.go
index a0766ef..a14f2f8 100644
--- a/graphql/client/client.go
+++ b/graphql/client/client.go
@@ -19,13 +19,10 @@ package client
 
 import (
 	"context"
-	"fmt"
-	"time"
 
 	"github.com/machinebox/graphql"
 	"github.com/urfave/cli"
 
-	"github.com/apache/skywalking-cli/graphql/schema"
 	"github.com/apache/skywalking-cli/logger"
 )
 
@@ -37,6 +34,7 @@ func newClient(cliCtx *cli.Context) (client *graphql.Client) {
 	return
 }
 
+// ExecuteQuery executes the `request` and parse to the `response`, returning `error` if there is any.
 func ExecuteQuery(cliCtx *cli.Context, request *graphql.Request, response interface{}) error {
 	client := newClient(cliCtx)
 	ctx := context.Background()
@@ -44,151 +42,9 @@ func ExecuteQuery(cliCtx *cli.Context, request *graphql.Request, response interf
 	return err
 }
 
+// ExecuteQuery executes the `request` and parse to the `response`, panic if there is any `error`.
 func ExecuteQueryOrFail(cliCtx *cli.Context, request *graphql.Request, response interface{}) {
-	client := newClient(cliCtx)
-	ctx := context.Background()
-	if err := client.Run(ctx, request, response); err != nil {
-		logger.Log.Fatalln(err)
-	}
-}
-
-func Services(cliCtx *cli.Context, duration schema.Duration) []schema.Service {
-	var response map[string][]schema.Service
-	request := graphql.NewRequest(`
-		query ($duration: Duration!) {
-			services: getAllServices(duration: $duration) {
-				id name
-			}
-		}
-	`)
-	request.Var("duration", duration)
-
-	ExecuteQueryOrFail(cliCtx, request, &response)
-	return response["services"]
-}
-
-func SearchEndpoints(cliCtx *cli.Context, serviceID, keyword string, limit int) []schema.Endpoint {
-	var response map[string][]schema.Endpoint
-	request := graphql.NewRequest(`
-		query ($keyword: String!, $serviceId: ID!, $limit: Int!) {
-			endpoints: searchEndpoint(keyword: $keyword, serviceId: $serviceId, limit: $limit) {
-				id name
-			}
-		}
-	`)
-	request.Var("serviceId", serviceID)
-	request.Var("keyword", keyword)
-	request.Var("limit", limit)
-
-	ExecuteQueryOrFail(cliCtx, request, &response)
-	return response["endpoints"]
-}
-
-func GetEndpointInfo(cliCtx *cli.Context, endpointID string) schema.Endpoint {
-	var response map[string]schema.Endpoint
-	request := graphql.NewRequest(`
-		query ($endpointId: ID!) {
-			endpoint: getEndpointInfo(endpointId: $endpointId) {
-				id name
-			}
-		}
-	`)
-	request.Var("endpointId", endpointID)
-
-	ExecuteQueryOrFail(cliCtx, request, &response)
-	return response["endpoint"]
-}
-
-func Instances(cliCtx *cli.Context, serviceID string, duration schema.Duration) []schema.ServiceInstance {
-	var response map[string][]schema.ServiceInstance
-	request := graphql.NewRequest(`
-		query ($serviceId: ID!, $duration: Duration!) {
-			instances: getServiceInstances(duration: $duration, serviceId: $serviceId) {
-				id
-				name
-				language
-				instanceUUID
-				attributes {
-					name
-					value
-				}
-			}
-		}
-	`)
-	request.Var("serviceId", serviceID)
-	request.Var("duration", duration)
-
-	ExecuteQueryOrFail(cliCtx, request, &response)
-	return response["instances"]
-}
-
-func SearchService(cliCtx *cli.Context, serviceCode string) (service schema.Service, err error) {
-	var response map[string]schema.Service
-	request := graphql.NewRequest(`
-		query searchService($serviceCode: String!) {
-			service: searchService(serviceCode: $serviceCode) {
-				id name
-			}
-		}
-	`)
-	request.Var("serviceCode", serviceCode)
-
-	ExecuteQueryOrFail(cliCtx, request, &response)
-	service = response["service"]
-	if service.ID == "" {
-		return service, fmt.Errorf("no such service [%s]", serviceCode)
-	}
-	return service, nil
-}
-
-func LinearIntValues(ctx *cli.Context, condition schema.MetricCondition, duration schema.Duration) map[string]float64 {
-	var response map[string]schema.IntValues
-
-	request := graphql.NewRequest(`
-		query ($metric: MetricCondition!, $duration: Duration!) {
-			metrics: getLinearIntValues(metric: $metric, duration: $duration) {
-				values { value }
-			}
-		}
-	`)
-	request.Var("metric", condition)
-	request.Var("duration", duration)
-
-	ExecuteQueryOrFail(ctx, request, &response)
-
-	values := metricsToMap(duration, response["metrics"].Values)
-
-	return values
-}
-
-func IntValues(ctx *cli.Context, condition schema.BatchMetricConditions, duration schema.Duration) []*schema.KVInt {
-	var response map[string]schema.IntValues
-
-	request := graphql.NewRequest(`
-		query ($metric: BatchMetricConditions!, $duration: Duration!) {
-			metrics: getValues(metric: $metric, duration: $duration) {
-				values { id value }
-			}
-		}
-	`)
-	request.Var("metric", condition)
-	request.Var("duration", duration)
-
-	ExecuteQueryOrFail(ctx, request, &response)
-
-	return response["metrics"].Values
-}
-
-func metricsToMap(duration schema.Duration, kvInts []*schema.KVInt) map[string]float64 {
-	values := map[string]float64{}
-	format := schema.StepFormats[duration.Step]
-	startTime, err := time.Parse(format, duration.Start)
-	if err != nil {
+	if err := ExecuteQuery(cliCtx, request, response); err != nil {
 		logger.Log.Fatalln(err)
 	}
-	step := schema.StepDuration[duration.Step]
-	for idx, value := range kvInts {
-		values[startTime.Add(time.Duration(idx)*step).Format(format)] = float64(value.Value)
-	}
-	return values
 }
diff --git a/graphql/metadata/metadata.go b/graphql/metadata/metadata.go
index 55a1bc7..93b6875 100644
--- a/graphql/metadata/metadata.go
+++ b/graphql/metadata/metadata.go
@@ -18,6 +18,8 @@
 package metadata
 
 import (
+	"fmt"
+
 	"github.com/machinebox/graphql"
 	"github.com/urfave/cli"
 
@@ -25,7 +27,97 @@ import (
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
+func AllServices(cliCtx *cli.Context, duration schema.Duration) []schema.Service {
+	var response map[string][]schema.Service
+	request := graphql.NewRequest(`
+		query ($duration: Duration!) {
+			services: getAllServices(duration: $duration) {
+				id name
+			}
+		}
+	`)
+	request.Var("duration", duration)
+
+	client.ExecuteQueryOrFail(cliCtx, request, &response)
+	return response["services"]
+}
+
+func SearchService(cliCtx *cli.Context, serviceCode string) (service schema.Service, err error) {
+	var response map[string]schema.Service
+	request := graphql.NewRequest(`
+		query searchService($serviceCode: String!) {
+			service: searchService(serviceCode: $serviceCode) {
+				id name
+			}
+		}
+	`)
+	request.Var("serviceCode", serviceCode)
+
+	client.ExecuteQueryOrFail(cliCtx, request, &response)
+	service = response["service"]
+	if service.ID == "" {
+		return service, fmt.Errorf("no such service [%s]", serviceCode)
+	}
+	return service, nil
+}
+
+func SearchEndpoints(cliCtx *cli.Context, serviceID, keyword string, limit int) []schema.Endpoint {
+	var response map[string][]schema.Endpoint
+	request := graphql.NewRequest(`
+		query ($keyword: String!, $serviceId: ID!, $limit: Int!) {
+			endpoints: searchEndpoint(keyword: $keyword, serviceId: $serviceId, limit: $limit) {
+				id name
+			}
+		}
+	`)
+	request.Var("serviceId", serviceID)
+	request.Var("keyword", keyword)
+	request.Var("limit", limit)
+
+	client.ExecuteQueryOrFail(cliCtx, request, &response)
+	return response["endpoints"]
+}
+
+func EndpointInfo(cliCtx *cli.Context, endpointID string) schema.Endpoint {
+	var response map[string]schema.Endpoint
+	request := graphql.NewRequest(`
+		query ($endpointId: ID!) {
+			endpoint: getEndpointInfo(endpointId: $endpointId) {
+				id name
+			}
+		}
+	`)
+	request.Var("endpointId", endpointID)
+
+	client.ExecuteQueryOrFail(cliCtx, request, &response)
+	return response["endpoint"]
+}
+
+func Instances(cliCtx *cli.Context, serviceID string, duration schema.Duration) []schema.ServiceInstance {
+	var response map[string][]schema.ServiceInstance
+	request := graphql.NewRequest(`
+		query ($serviceId: ID!, $duration: Duration!) {
+			instances: getServiceInstances(duration: $duration, serviceId: $serviceId) {
+				id
+				name
+				language
+				instanceUUID
+				attributes {
+					name
+					value
+				}
+			}
+		}
+	`)
+	request.Var("serviceId", serviceID)
+	request.Var("duration", duration)
+
+	client.ExecuteQueryOrFail(cliCtx, request, &response)
+	return response["instances"]
+}
+
 func ServerTimeInfo(cliCtx *cli.Context) (schema.TimeInfo, error) {
+	var response map[string]schema.TimeInfo
 	request := graphql.NewRequest(`
 		query {
 			timeInfo: getTimeInfo {
@@ -34,7 +126,6 @@ func ServerTimeInfo(cliCtx *cli.Context) (schema.TimeInfo, error) {
 		}
 	`)
 
-	var response map[string]schema.TimeInfo
 	if err := client.ExecuteQuery(cliCtx, request, &response); err != nil {
 		return schema.TimeInfo{}, err
 	}
diff --git a/graphql/metrics/metrics.go b/graphql/metrics/metrics.go
new file mode 100644
index 0000000..36187d1
--- /dev/null
+++ b/graphql/metrics/metrics.go
@@ -0,0 +1,64 @@
+// Licensed to 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. Apache Software Foundation (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 metrics
+
+import (
+	"github.com/machinebox/graphql"
+	"github.com/urfave/cli"
+
+	"github.com/apache/skywalking-cli/graphql/client"
+
+	"github.com/apache/skywalking-cli/graphql/schema"
+)
+
+func IntValues(ctx *cli.Context, condition schema.BatchMetricConditions, duration schema.Duration) schema.IntValues {
+	var response map[string]schema.IntValues
+
+	request := graphql.NewRequest(`
+		query ($metric: BatchMetricConditions!, $duration: Duration!) {
+			metrics: getValues(metric: $metric, duration: $duration) {
+				values { id value }
+			}
+		}
+	`)
+	request.Var("metric", condition)
+	request.Var("duration", duration)
+
+	client.ExecuteQueryOrFail(ctx, request, &response)
+
+	return response["metrics"]
+}
+
+func MultipleLinearIntValues(ctx *cli.Context, condition schema.MetricCondition, numOfLinear int, duration schema.Duration) []schema.IntValues {
+	request := graphql.NewRequest(`
+		query ($metric: MetricCondition!, $numOfLinear: Int!, $duration: Duration!) {
+			metrics: getMultipleLinearIntValues(metric: $metric, numOfLinear: $numOfLinear, duration: $duration) {
+				values { value }
+			}
+		}
+	`)
+	request.Var("metric", condition)
+	request.Var("numOfLinear", numOfLinear)
+	request.Var("duration", duration)
+
+	var response map[string][]schema.IntValues
+
+	client.ExecuteQueryOrFail(ctx, request, &response)
+
+	return response["metrics"]
+}
diff --git a/graphql/schema/constants.go b/graphql/utils/constants.go
similarity index 64%
rename from graphql/schema/constants.go
rename to graphql/utils/constants.go
index f146338..b4f0f3d 100644
--- a/graphql/schema/constants.go
+++ b/graphql/utils/constants.go
@@ -15,24 +15,28 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package schema
+package utils
 
-import "time"
+import (
+	"time"
+
+	"github.com/apache/skywalking-cli/graphql/schema"
+)
 
 // StepFormats is a mapping from schema.Step to its time format
-var StepFormats = map[Step]string{
-	StepSecond: "2006-01-02 150400",
-	StepMinute: "2006-01-02 1504",
-	StepHour:   "2006-01-02 15",
-	StepDay:    "2006-01-02",
-	StepMonth:  "2006-01",
+var StepFormats = map[schema.Step]string{
+	schema.StepSecond: "2006-01-02 150400",
+	schema.StepMinute: "2006-01-02 1504",
+	schema.StepHour:   "2006-01-02 15",
+	schema.StepDay:    "2006-01-02",
+	schema.StepMonth:  "2006-01",
 }
 
 // StepDuration is a mapping from schema.Step to its time.Duration
-var StepDuration = map[Step]time.Duration{
-	StepSecond: time.Second,
-	StepMinute: time.Minute,
-	StepHour:   time.Hour,
-	StepDay:    time.Hour * 24,
-	StepMonth:  time.Hour * 24 * 30,
+var StepDuration = map[schema.Step]time.Duration{
+	schema.StepSecond: time.Second,
+	schema.StepMinute: time.Minute,
+	schema.StepHour:   time.Hour,
+	schema.StepDay:    time.Hour * 24,
+	schema.StepMonth:  time.Hour * 24 * 30,
 }
diff --git a/display/graph/graph.go b/graphql/utils/strings.go
similarity index 69%
copy from display/graph/graph.go
copy to graphql/utils/strings.go
index 7184360..9e61143 100644
--- a/display/graph/graph.go
+++ b/graphql/utils/strings.go
@@ -15,21 +15,21 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package graph
+package utils
 
 import (
-	"fmt"
-	"reflect"
+	"encoding/json"
 
-	"github.com/apache/skywalking-cli/display/graph/linear"
+	"github.com/apache/skywalking-cli/graphql/schema"
+	"github.com/apache/skywalking-cli/logger"
 )
 
-func Display(object interface{}) error {
-	if reflect.TypeOf(object) != reflect.TypeOf(map[string]float64{}) {
-		return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(object))
-	}
-
-	kvs := object.(map[string]float64)
+type IntValues schema.IntValues
 
-	return linear.Display(kvs)
+func (intValues *IntValues) String() string {
+	bytes, err := json.Marshal(intValues.Values)
+	if err != nil {
+		logger.Log.Fatalln(err)
+	}
+	return string(bytes)
 }