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/08/29 15:45:35 UTC

[skywalking-cli] branch master updated: Refactor `metrics thermodynamic` command (#59)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new b47c5fb  Refactor `metrics thermodynamic` command (#59)
b47c5fb is described below

commit b47c5fb913c7e9e2472ae4837880510a8780731c
Author: Hoshea Jiang <fg...@gmail.com>
AuthorDate: Sat Aug 29 23:45:28 2020 +0800

    Refactor `metrics thermodynamic` command (#59)
---
 README.md                                       |   6 +-
 assets/graphqls/metrics/Thermodynamic.graphql   |  13 ++-
 commands/metrics/thermodynamic/thermodynamic.go |  31 +++---
 commands/model/scope.go                         |  52 +++++++++
 display/graph/graph.go                          |   2 +-
 display/graph/heatmap/heatmap.go                | 140 ++++++++++++------------
 graphql/metrics/metrics.go                      |   6 +-
 lib/heatmap.go                                  | 114 -------------------
 8 files changed, 156 insertions(+), 208 deletions(-)

diff --git a/README.md b/README.md
index 3a96ab5..4c634c8 100644
--- a/README.md
+++ b/README.md
@@ -222,12 +222,12 @@ Ascii Graph, like coloring in terminal, so please use `json`  or `yaml` instead.
 
 <details>
 
-<summary>metrics thermodynamic --name=thermodynamic name --ids=entity-ids</summary>
+<summary>metrics thermodynamic --name=thermodynamic name [--scope=scope-of-metrics]</summary>
 
 | option | description | default |
 | :--- | :--- | :--- |
 | `--name` | Metrics name, defined in [OAL](https://github.com/apache/skywalking/blob/master/oap-server/server-bootstrap/src/main/resources/official_analysis.oal), such as `service_sla`, etc. |
-| `--ids` | IDs that are required by the metric type, such as service IDs for `service_heatmap` |
+| `--scope` | The scope of metrics, which is consistent with `--name`, such as `All`, `Service`, etc. |`All`|
 | `--start` | See [Common options](#common-options) | See [Common options](#common-options) |
 | `--end` | See [Common options](#common-options) | See [Common options](#common-options) |
 
@@ -567,7 +567,7 @@ $ ./bin/swctl metrics top 5 --name endpoint_sla --service-id 3
 
 ```shell
 $ ./bin/swctl metrics thermodynamic --name all_heatmap
-{"nodes":[[0,0,238],[0,1,1],[0,2,39],[0,3,31],[0,4,12],[0,5,13],[0,6,4],[0,7,3],[0,8,3],[0,9,0],[0,10,48],[0,11,3],[0,12,49],[0,13,54],[0,14,11],[0,15,9],[0,16,2],[0,17,4],[0,18,0],[0,19,1],[0,20,186],[1,0,264],[1,1,3],[1,2,51],[1,3,38],[1,4,16],[1,5,14],[1,6,3],[1,7,2],[1,8,1],[1,9,2],[1,10,51],[1,11,1],[1,12,41],[1,13,56],[1,14,16],[1,15,15],[1,16,7],[1,17,7],[1,18,3],[1,19,1],[1,20,174],[2,0,231],[2,1,3],[2,2,42],[2,3,41],[2,4,18],[2,5,4],[2,6,2],[2,7,1],[2,8,2],[2,9,0],[2,10,54],[2,1 [...]
+{"values":[{"id":"202008290939","values":[473,3,0,0,0,0,0,0,0,0,323,0,4,0,0,0,0,0,0,0,436]},{"id":"202008290940","values":[434,0,0,0,0,0,0,0,0,0,367,0,4,0,0,0,0,0,0,0,427]},{"id":"202008290941","values":[504,0,0,0,0,0,0,0,0,0,410,0,5,0,1,0,0,0,0,0,377]},{"id":"202008290942","values":[445,0,4,0,0,0,0,0,0,0,350,0,0,0,0,0,0,0,0,0,420]},{"id":"202008290943","values":[436,0,1,0,0,0,0,0,0,0,367,0,3,0,0,0,0,0,0,0,404]},{"id":"202008290944","values":[463,0,0,0,0,0,0,0,0,0,353,0,0,0,0,0,0,0,0,0,4 [...]
 ```
 
 ```shell
diff --git a/assets/graphqls/metrics/Thermodynamic.graphql b/assets/graphqls/metrics/Thermodynamic.graphql
index 9ff50bd..ddbbe81 100644
--- a/assets/graphqls/metrics/Thermodynamic.graphql
+++ b/assets/graphqls/metrics/Thermodynamic.graphql
@@ -15,8 +15,15 @@
 # specific language governing permissions and limitations
 # under the License.
 
-query ($metric: MetricCondition!, $duration: Duration!) {
-    result: getThermodynamic(metric: $metric, duration: $duration) {
-        nodes axisYStep
+query ($condition: MetricsCondition!, $duration: Duration!) {
+    result: readHeatMap(condition: $condition, duration: $duration) {
+        values {
+            id
+            values
+        }
+        buckets {
+            min
+            max
+        }
     }
 }
diff --git a/commands/metrics/thermodynamic/thermodynamic.go b/commands/metrics/thermodynamic/thermodynamic.go
index 096a921..62268e1 100644
--- a/commands/metrics/thermodynamic/thermodynamic.go
+++ b/commands/metrics/thermodynamic/thermodynamic.go
@@ -31,9 +31,9 @@ import (
 )
 
 var Command = cli.Command{
-	Name:      "thermodynamic",
-	ShortName: "td",
-	Usage:     "Query thermodynamic metrics defined in backend OAL",
+	Name:    "thermodynamic",
+	Aliases: []string{"td", "heatmap", "hp"},
+	Usage:   "Query thermodynamic metrics defined in backend OAL",
 	Flags: flags.Flags(
 		flags.DurationFlags,
 		[]cli.Flag{
@@ -42,10 +42,14 @@ var Command = cli.Command{
 				Usage:    "metrics `name`, which should be defined in OAL script",
 				Required: true,
 			},
-			cli.StringFlag{
-				Name:     "id",
-				Usage:    "metrics `id` if the metrics require one",
-				Required: false,
+			cli.GenericFlag{
+				Name:  "scope",
+				Usage: "the scope of the query, which follows the metrics `name`",
+				Value: &model.ScopeEnumValue{
+					Enum:     schema.AllScope,
+					Default:  schema.ScopeAll,
+					Selected: schema.ScopeAll,
+				},
 			},
 		},
 	),
@@ -58,12 +62,7 @@ var Command = cli.Command{
 		start := ctx.String("start")
 		step := ctx.Generic("step")
 		metricsName := ctx.String("name")
-
-		var id *string = nil
-		if ctx.String("id") != "" {
-			idString := ctx.String("id")
-			id = &idString
-		}
+		scope := ctx.Generic("scope").(*model.ScopeEnumValue).Selected
 
 		duration := schema.Duration{
 			Start: start,
@@ -71,9 +70,11 @@ var Command = cli.Command{
 			Step:  step.(*model.StepEnumValue).Selected,
 		}
 
-		metricsValues := metrics.Thermodynamic(ctx, schema.MetricCondition{
+		metricsValues := metrics.Thermodynamic(ctx, schema.MetricsCondition{
 			Name: metricsName,
-			ID:   id,
+			Entity: &schema.Entity{
+				Scope: scope,
+			},
 		}, duration)
 
 		return display.Display(ctx, &displayable.Displayable{
diff --git a/commands/model/scope.go b/commands/model/scope.go
new file mode 100644
index 0000000..7adc5e2
--- /dev/null
+++ b/commands/model/scope.go
@@ -0,0 +1,52 @@
+// 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 model
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/apache/skywalking-cli/graphql/schema"
+)
+
+// ScopeEnumValue defines the values domain of --scope option
+type ScopeEnumValue struct {
+	Enum     []schema.Scope
+	Default  schema.Scope
+	Selected schema.Scope
+}
+
+// Set the --scope value, from raw string to ScopeEnumValue
+func (s *ScopeEnumValue) Set(value string) error {
+	for _, enum := range s.Enum {
+		if strings.EqualFold(enum.String(), value) {
+			s.Selected = enum
+			return nil
+		}
+	}
+	scopes := make([]string, len(schema.AllScope))
+	for i, scope := range schema.AllScope {
+		scopes[i] = scope.String()
+	}
+	return fmt.Errorf("allowed scopes are %s", strings.Join(scopes, ", "))
+}
+
+// String representation of the scope
+func (s ScopeEnumValue) String() string {
+	return s.Selected.String()
+}
diff --git a/display/graph/graph.go b/display/graph/graph.go
index ddc37da..73dd51e 100644
--- a/display/graph/graph.go
+++ b/display/graph/graph.go
@@ -36,7 +36,7 @@ import (
 )
 
 type (
-	Thermodynamic      = schema.Thermodynamic
+	Thermodynamic      = schema.HeatMap
 	LinearMetrics      = map[string]float64
 	MultiLinearMetrics = []LinearMetrics
 	Trace              = schema.Trace
diff --git a/display/graph/heatmap/heatmap.go b/display/graph/heatmap/heatmap.go
index 0ca653a..1a40d29 100644
--- a/display/graph/heatmap/heatmap.go
+++ b/display/graph/heatmap/heatmap.go
@@ -18,97 +18,99 @@
 package heatmap
 
 import (
+	"context"
 	"fmt"
-	"math"
-	"time"
+	"strings"
 
-	"github.com/apache/skywalking-cli/graphql/utils"
-	"github.com/apache/skywalking-cli/util"
-
-	ui "github.com/gizak/termui/v3"
+	"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/widgetapi"
 
 	d "github.com/apache/skywalking-cli/display/displayable"
 	"github.com/apache/skywalking-cli/graphql/schema"
-	"github.com/apache/skywalking-cli/lib"
+	"github.com/apache/skywalking-cli/graphql/utils"
+	"github.com/apache/skywalking-cli/lib/heatmap"
 )
 
-func Display(displayable *d.Displayable) error {
-	data := displayable.Data.(schema.Thermodynamic)
-
-	nodes := data.Nodes
-	duration := displayable.Duration
-
-	rows, cols, min, max := statistics(nodes)
+const rootID = "root"
 
-	if err := ui.Init(); err != nil {
-		return err
+func NewHeatMapWidget(data schema.HeatMap) (hp *heatmap.HeatMap, err error) {
+	hp, err = heatmap.NewHeatMap()
+	if err != nil {
+		return hp, err
 	}
-	defer ui.Close()
 
-	termW, _ := ui.TerminalDimensions()
+	hpColumns := utils.HeatMapToMap(&data)
+	yLabels := utils.BucketsToStrings(data.Buckets)
+	hp.SetColumns(hpColumns)
+	hp.SetYLabels(yLabels)
 
-	hm := lib.NewHeatMap()
-	hm.Title = fmt.Sprintf(" %s ", displayable.Title)
-	hm.XLabels = make([]string, rows)
-	hm.YLabels = make([]string, cols)
-	for i := 0; i < rows; i++ {
-		step := utils.StepDuration[duration.Step]
-		format := utils.StepFormats[duration.Step]
-		startTime, err := time.Parse(format, duration.Start)
+	return
+}
 
-		if err != nil {
-			return err
-		}
+// layout controls where and how the heat map widget is placed.
+// Here uses the grid layout to center the widget horizontally and vertically.
+func layout(hp widgetapi.Widget) ([]container.Option, error) {
+	const hpColWidthPerc = 85
+	const hpRowHeightPerc = 80
+
+	builder := grid.New()
+	builder.Add(
+		grid.ColWidthPerc((99-hpColWidthPerc)/2), // Use two empty cols to center the heatmap.
+		grid.ColWidthPerc(hpColWidthPerc,
+			grid.RowHeightPerc((99-hpRowHeightPerc)/2), // Use two empty rows to center the heatmap.
+			grid.RowHeightPerc(hpRowHeightPerc, grid.Widget(hp)),
+			grid.RowHeightPerc((99-hpRowHeightPerc)/2),
+		),
+		grid.ColWidthPerc((99-hpColWidthPerc)/2),
+	)
+	return builder.Build()
+}
 
-		hm.XLabels[i] = startTime.Add(time.Duration(i) * step).Format("15:04")
+func Display(displayable *d.Displayable) error {
+	t, err := termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
+	if err != nil {
+		return err
 	}
-	for i := 0; i < cols; i++ {
-		hm.YLabels[i] = fmt.Sprintf("%4d", i*data.AxisYStep)
+	defer t.Close()
+
+	title := fmt.Sprintf("[%s]-PRESS Q TO QUIT", displayable.Title)
+	c, err := container.New(
+		t,
+		container.Border(linestyle.Light),
+		container.BorderTitle(title),
+		container.ID(rootID))
+	if err != nil {
+		return err
 	}
 
-	hm.Data = make([][]float64, rows)
-	hm.CellColors = make([][]ui.Color, rows)
-	hm.NumStyles = make([][]ui.Style, rows)
-	for row := 0; row < rows; row++ {
-		hm.Data[row] = make([]float64, cols)
-		hm.CellColors[row] = make([]ui.Color, cols)
-		hm.NumStyles[row] = make([]ui.Style, cols)
+	data := displayable.Data.(schema.HeatMap)
+	hp, err := NewHeatMapWidget(data)
+	if err != nil {
+		return err
 	}
 
-	scale := max - min
-	for _, node := range nodes {
-		color := ui.Color(255 - (float64(*node[2])/scale)*23)
-		hm.Data[*node[0]][*node[1]] = float64(*node[2])
-		hm.CellColors[*node[0]][*node[1]] = color
-		hm.NumStyles[*node[0]][*node[1]] = ui.Style{Fg: ui.ColorMagenta}
+	gridOpts, err := layout(hp)
+	if err != nil {
+		return fmt.Errorf("builder.Build => %v", err)
 	}
 
-	hm.Formatter = nil
-	hm.XLabelStyles = []ui.Style{{Fg: ui.ColorWhite}}
-	hm.CellGap = 0
-	hm.CellWidth = int(float64(termW) / float64(rows))
-	realWidth := (hm.CellWidth+hm.CellGap)*(rows+1) - hm.CellGap + 5
-	hm.SetRect(int(float64(termW-realWidth)/2), 2, realWidth, cols+5)
-
-	ui.Render(hm)
-
-	events := ui.PollEvents()
-	for e := <-events; e.ID != "q" && e.ID != "<C-c>"; e = <-events {
+	if e := c.Update(rootID, gridOpts...); e != nil {
+		return e
 	}
-	return nil
-}
-
-func statistics(nodes [][]*int) (rows, cols int, min, max float64) {
-	min = math.MaxFloat64
 
-	for _, node := range nodes {
-		rows = util.MaxInt(rows, *node[0])
-		cols = util.MaxInt(cols, *node[1])
-		max = math.Max(max, float64(*node[2]))
-		min = math.Min(min, float64(*node[2]))
+	con, cancel := context.WithCancel(context.Background())
+	quitter := func(keyboard *terminalapi.Keyboard) {
+		if strings.EqualFold(keyboard.Key.String(), "q") {
+			cancel()
+		}
 	}
 
-	rows++
-	cols++
-	return
+	err = termdash.Run(con, t, c, termdash.KeyboardSubscriber(quitter))
+
+	return err
 }
diff --git a/graphql/metrics/metrics.go b/graphql/metrics/metrics.go
index 70a0422..0cf7099 100644
--- a/graphql/metrics/metrics.go
+++ b/graphql/metrics/metrics.go
@@ -68,12 +68,12 @@ func MultipleLinearIntValues(ctx *cli.Context, condition schema.MetricCondition,
 	return response["result"]
 }
 
-func Thermodynamic(ctx *cli.Context, condition schema.MetricCondition, duration schema.Duration) schema.Thermodynamic {
-	var response map[string]schema.Thermodynamic
+func Thermodynamic(ctx *cli.Context, condition schema.MetricsCondition, duration schema.Duration) schema.HeatMap {
+	var response map[string]schema.HeatMap
 
 	request := graphql.NewRequest(assets.Read("graphqls/metrics/Thermodynamic.graphql"))
 
-	request.Var("metric", condition)
+	request.Var("condition", condition)
 	request.Var("duration", duration)
 
 	client.ExecuteQueryOrFail(ctx, request, &response)
diff --git a/lib/heatmap.go b/lib/heatmap.go
deleted file mode 100644
index bf24b86..0000000
--- a/lib/heatmap.go
+++ /dev/null
@@ -1,114 +0,0 @@
-// 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 lib
-
-import (
-	"fmt"
-	im "image"
-
-	ui "github.com/gizak/termui/v3"
-	rw "github.com/mattn/go-runewidth"
-)
-
-type HeatMap struct {
-	ui.Block
-	XLabelStyles []ui.Style
-	CellColors   [][]ui.Color
-	NumStyles    [][]ui.Style
-	Formatter    func(float64) string
-	Data         [][]float64
-	XLabels      []string
-	YLabels      []string
-	CellWidth    int
-	CellGap      int
-}
-
-func NewHeatMap() *HeatMap {
-	return &HeatMap{
-		Block:        *ui.NewBlock(),
-		CellColors:   [][]ui.Color{ui.StandardColors, ui.StandardColors},
-		NumStyles:    [][]ui.Style{ui.StandardStyles, ui.StandardStyles},
-		Formatter:    func(n float64) string { return fmt.Sprint(n) },
-		XLabelStyles: ui.StandardStyles,
-		CellGap:      1,
-		CellWidth:    3,
-	}
-}
-
-func (hm *HeatMap) Draw(buffer *ui.Buffer) {
-	hm.Block.Draw(buffer)
-
-	cellX := hm.Inner.Min.X
-
-	for i, column := range hm.Data {
-		cellY := 0
-		for j, datum := range column {
-			buffer.SetString(
-				hm.YLabels[j],
-				ui.StyleClear,
-				im.Pt(hm.Inner.Min.X, (hm.Inner.Max.Y-2)-cellY),
-			)
-			for x := cellX + 5; x < ui.MinInt(cellX+hm.CellWidth, hm.Inner.Max.X)+5; x++ {
-				for y := (hm.Inner.Max.Y - 2) - cellY; y > (hm.Inner.Max.Y-2)-cellY-1; y-- {
-					cell := ui.NewCell(' ', ui.NewStyle(ui.ColorClear, color(hm.CellColors, i, j)))
-					buffer.SetCell(cell, im.Pt(x, y))
-				}
-			}
-
-			if hm.Formatter != nil {
-				hm.drawNumber(buffer, datum, i, j, cellX+5, cellY)
-			}
-
-			cellY++
-		}
-
-		if i < len(hm.XLabels) {
-			hm.drawLabel(buffer, cellX+5, i)
-		}
-
-		cellX += hm.CellWidth + hm.CellGap
-	}
-}
-
-func (hm *HeatMap) drawLabel(buffer *ui.Buffer, cellX, i int) {
-	labelX := cellX + ui.MaxInt(int(float64(hm.CellWidth)/2)-int(float64(rw.StringWidth(hm.XLabels[i]))/2), 0)
-	buffer.SetString(
-		ui.TrimString(hm.XLabels[i], hm.CellWidth),
-		ui.SelectStyle(hm.XLabelStyles, i),
-		im.Pt(labelX, hm.Inner.Max.Y-1),
-	)
-}
-
-func (hm *HeatMap) drawNumber(buffer *ui.Buffer, datum float64, i, j, cellX, cellY int) {
-	x := cellX + int(float64(hm.CellWidth)/2) - 1
-	numberStyle := style(hm.NumStyles, i, j)
-	cellColor := color(hm.CellColors, i, j)
-	buffer.SetString(
-		hm.Formatter(datum),
-		ui.NewStyle(numberStyle.Fg, cellColor, numberStyle.Modifier),
-		im.Pt(x, (hm.Inner.Max.Y-2)-cellY),
-	)
-}
-
-func color(colors [][]ui.Color, i, j int) ui.Color {
-	return colors[i%len(colors)][j%len(colors)]
-}
-
-func style(styles [][]ui.Style, i, j int) ui.Style {
-	return styles[i%len(styles)][j%len(styles)]
-}