pkg/display/graph/gauge/gauge.go (192 lines of code) (raw):

// 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 gauge import ( "context" "fmt" "math" "strconv" "strings" api "skywalking.apache.org/repo/goapi/query" "github.com/apache/skywalking-cli/pkg/contextkey" "github.com/apache/skywalking-cli/pkg/graphql/dashboard" "github.com/apache/skywalking-cli/pkg/util" "github.com/mum4k/termdash" "github.com/mum4k/termdash/cell" "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/gauge" "github.com/mum4k/termdash/widgets/text" ) const RootID = "root" type MetricColumn struct { title *text.Text gauges []*gauge.Gauge aggregationNum int } // Update updates the MetricColumn's `Absolute` and `BorderTitle`. func (mc *MetricColumn) Update(data []*api.SelectedRecord) error { for i, item := range data { // The number of `SelectedRecord` data may exceed the number of gauges in a `MetricColumn`. if i >= len(mc.gauges) { break } strValue := *(item.Value) v, err := strconv.Atoi(strValue) if err != nil { return err } if mc.aggregationNum != 0 { strValue = fmt.Sprintf("%.4f", float64(v)/float64(mc.aggregationNum)) } maxValue, err := findMaxValue(data) if err != nil { return err } if err := mc.gauges[i].Absolute(v, maxValue, gauge.BorderTitle("["+strValue+"]")); err != nil { return err } } return nil } func NewMetricColumn(column []*api.SelectedRecord, config *dashboard.MetricTemplate) (*MetricColumn, error) { var ret MetricColumn t, err := text.New() if err != nil { return nil, err } if err := t.Write(config.Title, text.WriteCellOpts(cell.FgColor(cell.ColorRed))); err != nil { return nil, err } ret.title = t if len(column) == 0 { return nil, fmt.Errorf("the metrics data is empty, please check the GraphQL backend") } for _, item := range column { strValue := *(item.Value) v, err := strconv.Atoi(strValue) if err != nil { return nil, err } if config.AggregationNum != "" { aggregationNum, convErr := strconv.Atoi(config.AggregationNum) if convErr != nil { return nil, convErr } strValue = fmt.Sprintf("%.4f", float64(v)/float64(aggregationNum)) ret.aggregationNum = aggregationNum } g, err := gauge.New( gauge.Height(1), gauge.Border(linestyle.Light), gauge.Color(cell.ColorMagenta), gauge.BorderTitle("["+strValue+"]"), gauge.HideTextProgress(), gauge.TextLabel(item.Name), ) if err != nil { return nil, err } maxValue, err := findMaxValue(column) if err != nil { return nil, err } if err := g.Absolute(v, maxValue); err != nil { return nil, err } ret.gauges = append(ret.gauges, g) } return &ret, nil } // 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 // For the best display effect, the maximum number of columns that can be displayed const MaxColumnNum = 4 // For the best display effect, the maximum number of gauges // that can be displayed in each column const MaxGaugeNum = 6 const TitleHeight = 10 // Number of columns to display, each column represents a global metric // The number should be less than or equal to MaxColumnNum columnNum := int(math.Min(MaxColumnNum, float64(len(columns)))) // columnWidthPerc should be in the range (0, 100) if columnNum > 1 { columnWidthPerc = 100 / columnNum } else { columnWidthPerc = 99 } for i := range columnNum { var column []grid.Element column = append(column, grid.RowHeightPerc(TitleHeight, grid.Widget(columns[i].title))) // Number of gauge in a column, each gauge represents a service or endpoint // The number should be less than or equal to MaxGaugeNum gaugeNum := int(math.Min(MaxGaugeNum, float64(len(columns[i].gauges)))) gaugeHeight := int(math.Floor(float64(99-TitleHeight) / float64(gaugeNum))) for j := range gaugeNum { column = append(column, grid.RowHeightPerc(gaugeHeight, grid.Widget(columns[i].gauges[j]))) } 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, columns...), ) gridOpts, err := builder.Build() if err != nil { return nil, err } return gridOpts, nil } func Display(ctx context.Context, metrics [][]*api.SelectedRecord) error { t, err := termbox.New() if err != nil { return err } defer t.Close() c, err := container.New( t, container.ID(RootID), ) if err != nil { return err } var columns []*MetricColumn configs, err := dashboard.LoadTemplate(ctx.Value(contextkey.DashboardTemplate{}).(string)) if err != nil { return nil } for i := range configs.Metrics { col, innerErr := NewMetricColumn(metrics[i], &configs.Metrics[i]) if innerErr != nil { return innerErr } columns = append(columns, col) } gridOpts, err := layout(MetricColumnsElement(columns)) if err != nil { return err } err = c.Update(RootID, append( gridOpts, container.Border(linestyle.Light), container.BorderTitle("[Global Metrics]-PRESS Q TO QUIT"))..., ) if err != nil { return err } 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 } // findMaxValue finds the maximum value in the array of `schema.SelectedRecord`. func findMaxValue(column []*api.SelectedRecord) (int, error) { var ret int for _, c := range column { v, err := strconv.Atoi(*(c.Value)) if err != nil { return ret, err } ret = util.MaxInt(ret, v) } return ret, nil }