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/02 05:21:51 UTC

[skywalking-cli] branch master updated: Integrate global metrics and global response latency into `dashboard global` command (#50)

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 1a0dc23  Integrate global metrics and global response latency into `dashboard global` command (#50)
1a0dc23 is described below

commit 1a0dc237f73fe54bdc914255c6bcb78af3977e34
Author: Hoshea Jiang <fg...@gmail.com>
AuthorDate: Sun Aug 2 13:21:41 2020 +0800

    Integrate global metrics and global response latency into `dashboard global` command (#50)
---
 assets/templates/Dashboard.Global.json |   5 +
 display/graph/dashboard/global.go      | 236 +++++++++++++++++++++++++++++++++
 display/graph/gauge/gauge.go           |  22 +--
 display/graph/graph.go                 |   8 +-
 display/graph/linear/linear.go         |  28 +++-
 example/Dashboard.Global.json          |   5 +
 graphql/dashboard/global.go            |  28 +++-
 7 files changed, 313 insertions(+), 19 deletions(-)

diff --git a/assets/templates/Dashboard.Global.json b/assets/templates/Dashboard.Global.json
index 74e55ba..d82c108 100644
--- a/assets/templates/Dashboard.Global.json
+++ b/assets/templates/Dashboard.Global.json
@@ -1,4 +1,9 @@
 {
+  "buttons": {
+    "texts": "Metrics, Response Latency",
+    "colorNumber": 220,
+    "height": 1
+  },
   "metrics": [
     {
       "condition": {
diff --git a/display/graph/dashboard/global.go b/display/graph/dashboard/global.go
new file mode 100644
index 0000000..445b541
--- /dev/null
+++ b/display/graph/dashboard/global.go
@@ -0,0 +1,236 @@
+// 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 dashboard
+
+import (
+	"context"
+	"math"
+	"strings"
+
+	"github.com/mattn/go-runewidth"
+	"github.com/mum4k/termdash"
+	"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/urfave/cli"
+
+	"github.com/apache/skywalking-cli/display/graph/gauge"
+	"github.com/apache/skywalking-cli/display/graph/linear"
+	"github.com/apache/skywalking-cli/graphql/dashboard"
+
+	"github.com/mum4k/termdash/cell"
+	"github.com/mum4k/termdash/container"
+	"github.com/mum4k/termdash/widgets/button"
+	"github.com/mum4k/termdash/widgets/linechart"
+)
+
+// rootID is the ID assigned to the root container.
+const rootID = "root"
+
+type layoutType int
+
+const (
+	// layoutAll displays all the widgets.
+	layoutAll layoutType = iota
+
+	// layoutLineChart focuses onto the line chart.
+	layoutLineChart
+)
+
+// widgets holds the widgets used by the dashboard.
+type widgets struct {
+	gauges  []*gauge.MetricColumn
+	linears []*linechart.LineChart
+
+	// buttons are used to change the layout.
+	buttons []*button.Button
+}
+
+// setLayout sets the specified layout.
+func setLayout(c *container.Container, w *widgets, lt layoutType) error {
+	gridOpts, err := gridLayout(w, lt)
+	if err != nil {
+		return err
+	}
+	return c.Update(rootID, gridOpts...)
+}
+
+// newLayoutButtons returns buttons that dynamically switch the layouts.
+func newLayoutButtons(c *container.Container, w *widgets, template *dashboard.ButtonTemplate) ([]*button.Button, error) {
+	var buttons []*button.Button
+
+	buttonTexts := strings.Split(template.Texts, ",")
+
+	opts := []button.Option{
+		button.WidthFor(longestString(buttonTexts)),
+		button.FillColor(cell.ColorNumber(template.ColorNum)),
+		button.Height(template.Height),
+	}
+
+	for i, text := range buttonTexts {
+		// declare a local variable lt to avoid closure.
+		lt := layoutType(i)
+
+		b, err := button.New(text, func() error {
+			return setLayout(c, w, lt)
+		}, opts...)
+		if err != nil {
+			return nil, err
+		}
+		buttons = append(buttons, b)
+	}
+
+	return buttons, nil
+}
+
+// gridLayout prepares container options that represent the desired screen layout.
+func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
+	const buttonRowHeight = 15
+
+	buttonColWidthPerc := 100 / len(w.buttons)
+	var buttonCols []grid.Element
+
+	for _, b := range w.buttons {
+		buttonCols = append(buttonCols, grid.ColWidthPerc(buttonColWidthPerc, grid.Widget(b)))
+	}
+
+	rows := []grid.Element{
+		grid.RowHeightPerc(buttonRowHeight, buttonCols...),
+	}
+
+	switch lt {
+	case layoutAll:
+		rows = append(rows,
+			grid.RowHeightPerc(70, gauge.MetricColumnsElement(w.gauges)...),
+		)
+
+	case layoutLineChart:
+		lcElements := linear.LineChartElements(w.linears)
+		percentage := int(math.Min(99, float64((100-buttonRowHeight)/len(lcElements))))
+
+		for _, e := range lcElements {
+			rows = append(rows,
+				grid.RowHeightPerc(percentage, e...),
+			)
+		}
+	}
+
+	builder := grid.New()
+	builder.Add(
+		grid.RowHeightPerc(99, rows...),
+	)
+	gridOpts, err := builder.Build()
+	if err != nil {
+		return nil, err
+	}
+	return gridOpts, nil
+}
+
+// newWidgets creates all widgets used by the dashboard.
+func newWidgets(data *dashboard.GlobalData, template *dashboard.GlobalTemplate) (*widgets, error) {
+	var columns []*gauge.MetricColumn
+	var linears []*linechart.LineChart
+
+	// Create gauges to display global metrics.
+	for i, t := range template.Metrics {
+		col, err := gauge.NewMetricColumn(data.Metrics[i], &t)
+		if err != nil {
+			return nil, err
+		}
+		columns = append(columns, col)
+	}
+
+	// Create line charts to display global response latency.
+	for _, input := range data.ResponseLatency {
+		l, err := linear.NewLineChart(input)
+		if err != nil {
+			return nil, err
+		}
+		linears = append(linears, l)
+	}
+
+	return &widgets{
+		gauges:  columns,
+		linears: linears,
+	}, nil
+}
+
+func Display(ctx *cli.Context, data *dashboard.GlobalData) error {
+	t, err := termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
+	if err != nil {
+		return err
+	}
+	defer t.Close()
+
+	c, err := container.New(
+		t,
+		container.Border(linestyle.Light),
+		container.BorderTitle("[Global Dashboard]-PRESS Q TO QUIT"),
+		container.ID(rootID))
+	if err != nil {
+		return err
+	}
+
+	template, err := dashboard.LoadTemplate(ctx.String("template"))
+	if err != nil {
+		return err
+	}
+
+	w, err := newWidgets(data, template)
+	if err != nil {
+		panic(err)
+	}
+	lb, err := newLayoutButtons(c, w, &template.Buttons)
+	if err != nil {
+		return err
+	}
+	w.buttons = lb
+
+	gridOpts, err := gridLayout(w, layoutAll)
+	if err != nil {
+		return err
+	}
+
+	if e := c.Update(rootID, gridOpts...); e != nil {
+		return e
+	}
+
+	con, cancel := context.WithCancel(context.Background())
+	quitter := func(keyboard *terminalapi.Keyboard) {
+		if strings.EqualFold(keyboard.Key.String(), "q") {
+			cancel()
+		}
+	}
+
+	err = termdash.Run(con, t, c, termdash.KeyboardSubscriber(quitter))
+
+	return err
+}
+
+// longestString returns the longest string in the string array.
+func longestString(strs []string) (ret string) {
+	maxLen := 0
+	for _, s := range strs {
+		if l := runewidth.StringWidth(s); l > maxLen {
+			ret = s
+			maxLen = l
+		}
+	}
+	return
+}
diff --git a/display/graph/gauge/gauge.go b/display/graph/gauge/gauge.go
index 4edf51c..5a0dde9 100644
--- a/display/graph/gauge/gauge.go
+++ b/display/graph/gauge/gauge.go
@@ -42,13 +42,13 @@ import (
 
 const RootID = "root"
 
-type metricColumn struct {
+type MetricColumn struct {
 	title  *text.Text
 	gauges []*gauge.Gauge
 }
 
-func newMetricColumn(column []*schema.SelectedRecord, config *dashboard.MetricTemplate) (*metricColumn, error) {
-	var ret metricColumn
+func NewMetricColumn(column []*schema.SelectedRecord, config *dashboard.MetricTemplate) (*MetricColumn, error) {
+	var ret MetricColumn
 	var maxValue int
 
 	t, err := text.New()
@@ -110,7 +110,9 @@ func newMetricColumn(column []*schema.SelectedRecord, config *dashboard.MetricTe
 	return &ret, nil
 }
 
-func layout(columns ...*metricColumn) ([]container.Option, error) {
+// MetricColumnsElement is the part that separated from layout,
+// which can be reused by global dashboard.
+func MetricColumnsElement(columns []*MetricColumn) []grid.Element {
 	var metricColumns []grid.Element
 	var columnWidthPerc int
 
@@ -147,10 +149,14 @@ func layout(columns ...*metricColumn) ([]container.Option, error) {
 		metricColumns = append(metricColumns, grid.ColWidthPerc(columnWidthPerc, column...))
 	}
 
+	return metricColumns
+}
+
+func layout(columns []grid.Element) ([]container.Option, error) {
 	builder := grid.New()
 	builder.Add(
 		grid.RowHeightPerc(10),
-		grid.RowHeightPerc(80, metricColumns...),
+		grid.RowHeightPerc(80, columns...),
 	)
 
 	gridOpts, err := builder.Build()
@@ -175,7 +181,7 @@ func Display(ctx *cli.Context, metrics [][]*schema.SelectedRecord) error {
 		return err
 	}
 
-	var columns []*metricColumn
+	var columns []*MetricColumn
 
 	configs, err := dashboard.LoadTemplate(ctx.String("template"))
 	if err != nil {
@@ -183,14 +189,14 @@ func Display(ctx *cli.Context, metrics [][]*schema.SelectedRecord) error {
 	}
 
 	for i, config := range configs.Metrics {
-		col, innerErr := newMetricColumn(metrics[i], &config)
+		col, innerErr := NewMetricColumn(metrics[i], &config)
 		if innerErr != nil {
 			return innerErr
 		}
 		columns = append(columns, col)
 	}
 
-	gridOpts, err := layout(columns...)
+	gridOpts, err := layout(MetricColumnsElement(columns))
 	if err != nil {
 		return err
 	}
diff --git a/display/graph/graph.go b/display/graph/graph.go
index c683c27..db68a16 100644
--- a/display/graph/graph.go
+++ b/display/graph/graph.go
@@ -23,9 +23,10 @@ import (
 
 	"github.com/urfave/cli"
 
+	db "github.com/apache/skywalking-cli/display/graph/dashboard"
 	"github.com/apache/skywalking-cli/display/graph/gauge"
-
 	"github.com/apache/skywalking-cli/display/graph/tree"
+	"github.com/apache/skywalking-cli/graphql/dashboard"
 
 	"github.com/apache/skywalking-cli/display/graph/heatmap"
 	"github.com/apache/skywalking-cli/graphql/schema"
@@ -40,6 +41,7 @@ type (
 	MultiLinearMetrics = []LinearMetrics
 	Trace              = schema.Trace
 	GlobalMetrics      = [][]*schema.SelectedRecord
+	GlobalData         = dashboard.GlobalData
 )
 
 var (
@@ -48,6 +50,7 @@ var (
 	MultiLinearMetricsType = reflect.TypeOf(MultiLinearMetrics{})
 	TraceType              = reflect.TypeOf(Trace{})
 	GlobalMetricsType      = reflect.TypeOf(GlobalMetrics{})
+	GlobalDataType         = reflect.TypeOf(&GlobalData{})
 )
 
 func Display(ctx *cli.Context, displayable *d.Displayable) error {
@@ -69,6 +72,9 @@ func Display(ctx *cli.Context, displayable *d.Displayable) error {
 	case GlobalMetricsType:
 		return gauge.Display(ctx, data.(GlobalMetrics))
 
+	case GlobalDataType:
+		return db.Display(ctx, data.(*GlobalData))
+
 	default:
 		return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(data))
 	}
diff --git a/display/graph/linear/linear.go b/display/graph/linear/linear.go
index c632112..a437524 100644
--- a/display/graph/linear/linear.go
+++ b/display/graph/linear/linear.go
@@ -21,6 +21,7 @@ import (
 	"context"
 	"fmt"
 	"math"
+	"sort"
 	"strings"
 
 	"github.com/mum4k/termdash/linestyle"
@@ -35,15 +36,22 @@ import (
 
 const RootID = "root"
 
-func newLineChart(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{}
 	yValues := make([]float64, len(inputs))
 
-	for xLabel, yValue := range inputs {
-		xLabels[index] = xLabel
-		yValues[index] = yValue
+	// The iteration order of map is uncertain, so the keys must be sorted explicitly.
+	var names []string
+	for name := range inputs {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+
+	for _, name := range names {
+		xLabels[index] = name
+		yValues[index] = inputs[name]
 		index++
 	}
 
@@ -56,7 +64,9 @@ func newLineChart(inputs map[string]float64) (lineChart *linechart.LineChart, er
 	return lineChart, err
 }
 
-func layout(lineCharts ...*linechart.LineChart) ([]container.Option, error) {
+// LineChartElements is the part that separated from layout,
+// which can be reused by global dashboard.
+func LineChartElements(lineCharts []*linechart.LineChart) [][]grid.Element {
 	cols := maxSqrt(len(lineCharts))
 
 	rows := make([][]grid.Element, int(math.Ceil(float64(len(lineCharts))/float64(cols))))
@@ -81,6 +91,10 @@ func layout(lineCharts ...*linechart.LineChart) ([]container.Option, error) {
 		rows[r] = row
 	}
 
+	return rows
+}
+
+func layout(rows [][]grid.Element) ([]container.Option, error) {
 	builder := grid.New()
 
 	for _, row := range rows {
@@ -109,14 +123,14 @@ func Display(inputs []map[string]float64) error {
 	var elements []*linechart.LineChart
 
 	for _, input := range inputs {
-		w, e := newLineChart(input)
+		w, e := NewLineChart(input)
 		if e != nil {
 			return e
 		}
 		elements = append(elements, w)
 	}
 
-	gridOpts, err := layout(elements...)
+	gridOpts, err := layout(LineChartElements(elements))
 	if err != nil {
 		return err
 	}
diff --git a/example/Dashboard.Global.json b/example/Dashboard.Global.json
index 74e55ba..d82c108 100644
--- a/example/Dashboard.Global.json
+++ b/example/Dashboard.Global.json
@@ -1,4 +1,9 @@
 {
+  "buttons": {
+    "texts": "Metrics, Response Latency",
+    "colorNumber": 220,
+    "height": 1
+  },
   "metrics": [
     {
       "condition": {
diff --git a/graphql/dashboard/global.go b/graphql/dashboard/global.go
index c0d2d8d..39b4202 100644
--- a/graphql/dashboard/global.go
+++ b/graphql/dashboard/global.go
@@ -21,6 +21,7 @@ import (
 	"encoding/json"
 	"io/ioutil"
 	"os"
+	"strconv"
 	"strings"
 
 	"github.com/machinebox/graphql"
@@ -29,8 +30,16 @@ import (
 	"github.com/apache/skywalking-cli/assets"
 	"github.com/apache/skywalking-cli/graphql/client"
 	"github.com/apache/skywalking-cli/graphql/schema"
+	"github.com/apache/skywalking-cli/graphql/utils"
+	"github.com/apache/skywalking-cli/logger"
 )
 
+type ButtonTemplate struct {
+	Texts    string `json:"texts"`
+	ColorNum int    `json:"colorNumber"`
+	Height   int    `json:"height"`
+}
+
 type MetricTemplate struct {
 	Condition      schema.TopNCondition `json:"condition"`
 	Title          string               `json:"title"`
@@ -46,6 +55,7 @@ type ChartTemplate struct {
 }
 
 type GlobalTemplate struct {
+	Buttons         ButtonTemplate   `json:"buttons"`
 	Metrics         []MetricTemplate `json:"metrics"`
 	ResponseLatency ChartTemplate    `json:"responseLatency"`
 	HeatMap         ChartTemplate    `json:"heatMap"`
@@ -53,7 +63,7 @@ type GlobalTemplate struct {
 
 type GlobalData struct {
 	Metrics         [][]*schema.SelectedRecord `json:"metrics"`
-	ResponseLatency []*schema.MetricsValues    `json:"responseLatency"`
+	ResponseLatency []map[string]float64       `json:"responseLatency"`
 	HeatMap         schema.HeatMap             `json:"heatMap"`
 }
 
@@ -115,7 +125,7 @@ func Metrics(ctx *cli.Context, duration schema.Duration) [][]*schema.SelectedRec
 	return ret
 }
 
-func responseLatency(ctx *cli.Context, duration schema.Duration) []*schema.MetricsValues {
+func responseLatency(ctx *cli.Context, duration schema.Duration) []map[string]float64 {
 	var response map[string][]*schema.MetricsValues
 
 	template, err := LoadTemplate(ctx.String("template"))
@@ -134,7 +144,19 @@ func responseLatency(ctx *cli.Context, duration schema.Duration) []*schema.Metri
 
 	client.ExecuteQueryOrFail(ctx, request, &response)
 
-	return response["result"]
+	// Convert metrics values to map type data.
+	responseLatency := response["result"]
+	reshaped := make([]map[string]float64, len(responseLatency))
+	for _, mvs := range responseLatency {
+		index, err := strconv.Atoi(strings.TrimSpace(*mvs.Label))
+		if err != nil {
+			logger.Log.Fatalln(err)
+			return nil
+		}
+		reshaped[index] = utils.MetricsToMap(duration, *mvs.Values)
+	}
+
+	return reshaped
 }
 
 func heatMap(ctx *cli.Context, duration schema.Duration) schema.HeatMap {