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 <[email protected]>
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 {

Reply via email to