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 {