[Feature] Support visualization of heat map and enhance Display logic (#34)
### Motivation
Visualize heat map and enhance Display logic
### Modification
- Add HeatMap widget to visualize thermodynamic metrics.
- Refactor display logic.
### Result
- Thermodynamic metrics now can be visualized.
- Display function takes more extra information to determine more details
- Closes https://github.com/apache/skywalking/issues/4461
diff --git a/README.md b/README.md
index 9fba39b..7e748b7 100644
--- a/README.md
+++ b/README.md
@@ -461,13 +461,17 @@
<details>
-<summary>Query the overall heatmap</summary>
+<summary>Query the overall heat map</summary>
```shell
-$ ./bin/swctl metrics thermodynamic --name all_heatmap
+$ ./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,11,4],[2,12,55],[2,13,48],[2,14,14],[2,15,4],[2,16,3],[2,17,2],[2,18,4],[2,19,4],[2,20,187],[3,0,231],[3,1,3],[3,2,55],[3,3,38],[3,4,18],[3,5,9],[3,6,1],[3,7,1],[3,8,1],[3,9,1],[3,10,56],[3,11,6],[3,12,38],[3,13,50],[3,14,16],[3,15,12],[3,16,4],[3,17,4],[3,18,2],[3,19,2],[3,20,183],[4,0,238],[4,1,2],[4,2,47],[4,3,49],[4,4,11],[4,5,7],[4,6,0],[4,7,0],[4,8,2],[4,9,2],[4,10,55],[4,11,3],[4,12,41],[4,13,47],[4,14,12],[4,15,7],[4,16,3],[4,17,2],[4,18,10],[4,19,0],[4,20,190],[5,0,238],[5,1,3],[5,2,42],[5,3,28],[5,4,18],[5,5,4],[5,6,2],[5,7,4],[5,8,4],[5,9,1],[5,10,54],[5,11,2],[5,12,65],[5,13,56],[5,14,17],[5,15,9],[5,16,2],[5,17,3],[5,18,0],[5,19,2],[5,20,179],[6,0,218],[6,1,1],[6,2,34],[6,3,37],[6,4,10],[6,5,5],[6,6,1],[6,7,1],[6,8,0],[6,9,3],[6,10,49],[6,11,7],[6,12,47],[6,13,43],[6,14,19],[6,15,15],[6,16,1],[6,17,4],[6,18,2],[6,19,3],[6,20,183],[7,0,242],[7,1,0],[7,2,41],[7,3,34],[7,4,21],[7,5,4],[7,6,3],[7,7,4],[7,8,1],[7,9,0],[7,10,71],[7,11,4],[7,12,47],[7,13,50],[7,14,19],[7,15,8],[7,16,6],[7,17,3],[7,18,2],[7,19,4],[7,20,174],[8,0,220],[8,1,3],[8,2,40],[8,3,36],[8,4,6],[8,5,8],[8,6,1],[8,7,5],[8,8,0],[8,9,1],[8,10,61],[8,11,2],[8,12,43],[8,13,50],[8,14,17],[8,15,11],[8,16,4],[8,17,5],[8,18,1],[8,19,1],[8,20,183],[9,0,239],[9,1,1],[9,2,48],[9,3,37],[9,4,8],[9,5,12],[9,6,2],[9,7,0],[9,8,0],[9,9,0],[9,10,74],[9,11,1],[9,12,58],[9,13,53],[9,14,17],[9,15,13],[9,16,5],[9,17,2],[9,18,2],[9,19,0],[9,20,178],[10,0,249],[10,1,2],[10,2,40],[10,3,49],[10,4,12],[10,5,8],[10,6,0],[10,7,1],[10,8,0],[10,9,0],[10,10,58],[10,11,1],[10,12,54],[10,13,47],[10,14,21],[10,15,12],[10,16,6],[10,17,4],[10,18,3],[10,19,2],[10,20,165],[11,0,240],[11,1,1],[11,2,50],[11,3,47],[11,4,10],[11,5,2],[11,6,1],[11,7,1],[11,8,2],[11,9,1],[11,10,52],[11,11,4],[11,12,41],[11,13,51],[11,14,17],[11,15,6],[11,16,1],[11,17,6],[11,18,1],[11,19,0],[11,20,199],[12,0,240],[12,1,3],[12,2,40],[12,3,41],[12,4,17],[12,5,10],[12,6,5],[12,7,2],[12,8,2],[12,9,0],[12,10,86],[12,11,1],[12,12,56],[12,13,49],[12,14,16],[12,15,7],[12,16,4],[12,17,8],[12,18,4],[12,19,3],[12,20,157],[13,0,234],[13,1,1],[13,2,53],[13,3,38],[13,4,12],[13,5,4],[13,6,0],[13,7,2],[13,8,0],[13,9,0],[13,10,59],[13,11,2],[13,12,53],[13,13,48],[13,14,18],[13,15,8],[13,16,3],[13,17,8],[13,18,1],[13,19,1],[13,20,187],[14,0,269],[14,1,0],[14,2,66],[14,3,47],[14,4,17],[14,5,4],[14,6,1],[14,7,0],[14,8,0],[14,9,0],[14,10,55],[14,11,1],[14,12,53],[14,13,48],[14,14,18],[14,15,8],[14,16,3],[14,17,3],[14,18,4],[14,19,0],[14,20,179],[15,0,254],[15,1,0],[15,2,57],[15,3,45],[15,4,8],[15,5,9],[15,6,9],[15,7,4],[15,8,3],[15,9,0],[15,10,68],[15,11,1],[15,12,52],[15,13,51],[15,14,19],[15,15,7],[15,16,4],[15,17,0],[15,18,0],[15,19,1],[15,20,177],[16,0,257],[16,1,1],[16,2,65],[16,3,50],[16,4,16],[16,5,3],[16,6,1],[16,7,0],[16,8,0],[16,9,0],[16,10,61],[16,11,3],[16,12,63],[16,13,59],[16,14,14],[16,15,9],[16,16,5],[16,17,2],[16,18,0],[16,19,0],[16,20,174],[17,0,243],[17,1,1],[17,2,63],[17,3,44],[17,4,5],[17,5,3],[17,6,0],[17,7,3],[17,8,0],[17,9,0],[17,10,66],[17,11,4],[17,12,56],[17,13,38],[17,14,11],[17,15,10],[17,16,4],[17,17,2],[17,18,3],[17,19,0],[17,20,181],[18,0,236],[18,1,3],[18,2,38],[18,3,49],[18,4,16],[18,5,5],[18,6,3],[18,7,3],[18,8,1],[18,9,0],[18,10,41],[18,11,4],[18,12,59],[18,13,49],[18,14,13],[18,15,9],[18,16,4],[18,17,1],[18,18,2],[18,19,0],[18,20,192],[19,0,238],[19,1,2],[19,2,49],[19,3,37],[19,4,15],[19,5,2],[19,6,1],[19,7,1],[19,8,3],[19,9,0],[19,10,60],[19,11,3],[19,12,58],[19,13,53],[19,14,17],[19,15,4],[19,16,2],[19,17,2],[19,18,2],[19,19,0],[19,20,185],[20,0,242],[20,1,0],[20,2,55],[20,3,36],[20,4,10],[20,5,6],[20,6,1],[20,7,1],[20,8,1],[20,9,0],[20,10,57],[20,11,4],[20,12,46],[20,13,58],[20,14,15],[20,15,11],[20,16,3],[20,17,2],[20,18,7],[20,19,0],[20,20,188],[21,0,231],[21,1,3],[21,2,50],[21,3,43],[21,4,13],[21,5,1],[21,6,0],[21,7,1],[21,8,0],[21,9,0],[21,10,57],[21,11,3],[21,12,51],[21,13,36],[21,14,15],[21,15,8],[21,16,7],[21,17,2],[21,18,3],[21,19,1],[21,20,188],[22,0,241],[22,1,2],[22,2,60],[22,3,42],[22,4,11],[22,5,8],[22,6,0],[22,7,0],[22,8,0],[22,9,0],[22,10,56],[22,11,4],[22,12,57],[22,13,46],[22,14,20],[22,15,8],[22,16,6],[22,17,1],[22,18,1],[22,19,0],[22,20,191],[23,0,240],[23,1,0],[23,2,46],[23,3,44],[23,4,20],[23,5,3],[23,6,3],[23,7,4],[23,8,1],[23,9,1],[23,10,62],[23,11,4],[23,12,64],[23,13,44],[23,14,15],[23,15,3],[23,16,4],[23,17,2],[23,18,3],[23,19,1],[23,20,181],[24,0,255],[24,1,0],[24,2,61],[24,3,41],[24,4,17],[24,5,7],[24,6,0],[24,7,1],[24,8,0],[24,9,0],[24,10,60],[24,11,3],[24,12,62],[24,13,49],[24,14,17],[24,15,10],[24,16,3],[24,17,2],[24,18,3],[24,19,2],[24,20,177],[25,0,244],[25,1,1],[25,2,56],[25,3,35],[25,4,12],[25,5,12],[25,6,2],[25,7,1],[25,8,0],[25,9,0],[25,10,66],[25,11,3],[25,12,53],[25,13,55],[25,14,20],[25,15,13],[25,16,3],[25,17,1],[25,18,3],[25,19,2],[25,20,173],[26,0,234],[26,1,1],[26,2,45],[26,3,34],[26,4,9],[26,5,6],[26,6,0],[26,7,3],[26,8,0],[26,9,1],[26,10,54],[26,11,6],[26,12,59],[26,13,48],[26,14,20],[26,15,10],[26,16,1],[26,17,2],[26,18,2],[26,19,0],[26,20,182],[27,0,228],[27,1,1],[27,2,46],[27,3,35],[27,4,5],[27,5,7],[27,6,2],[27,7,3],[27,8,2],[27,9,3],[27,10,61],[27,11,2],[27,12,61],[27,13,43],[27,14,15],[27,15,7],[27,16,3],[27,17,1],[27,18,3],[27,19,1],[27,20,187],[28,0,248],[28,1,4],[28,2,60],[28,3,45],[28,4,11],[28,5,9],[28,6,5],[28,7,1],[28,8,1],[28,9,1],[28,10,58],[28,11,2],[28,12,53],[28,13,38],[28,14,20],[28,15,10],[28,16,4],[28,17,6],[28,18,1],[28,19,2],[28,20,178],[29,0,241],[29,1,2],[29,2,46],[29,3,28],[29,4,16],[29,5,8],[29,6,4],[29,7,2],[29,8,1],[29,9,0],[29,10,66],[29,11,3],[29,12,51],[29,13,51],[29,14,28],[29,15,9],[29,16,3],[29,17,4],[29,18,3],[29,19,4],[29,20,153],[30,0,151],[30,1,1],[30,2,26],[30,3,26],[30,4,8],[30,5,4],[30,6,2],[30,7,2],[30,8,3],[30,9,1],[30,10,32],[30,11,3],[30,12,33],[30,13,25],[30,14,10],[30,15,3],[30,16,1],[30,17,3],[30,18,2],[30,19,0],[30,20,82]],"axisYStep":0}
```
+```shell
+$ ./bin/swctl --display=graph metrics thermodynamic --name all_heatmap
+```
+
</details>
<details>
diff --git a/commands/endpoint/list.go b/commands/endpoint/list.go
index 034c6a5..3c6767b 100644
--- a/commands/endpoint/list.go
+++ b/commands/endpoint/list.go
@@ -20,6 +20,8 @@
import (
"github.com/urfave/cli"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/metadata"
"github.com/apache/skywalking-cli/display"
@@ -56,6 +58,6 @@
endpoints := metadata.SearchEndpoints(ctx, serviceID, keyword, limit)
- return display.Display(ctx, endpoints)
+ return display.Display(ctx, &displayable.Displayable{Data: endpoints})
},
}
diff --git a/commands/instance/list.go b/commands/instance/list.go
index eda72c1..1cfcd88 100644
--- a/commands/instance/list.go
+++ b/commands/instance/list.go
@@ -20,6 +20,8 @@
import (
"github.com/urfave/cli"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/metadata"
"github.com/apache/skywalking-cli/commands/flags"
@@ -51,6 +53,6 @@
Step: step.(*model.StepEnumValue).Selected,
})
- return display.Display(ctx, instances)
+ return display.Display(ctx, &displayable.Displayable{Data: instances})
},
}
diff --git a/commands/instance/search.go b/commands/instance/search.go
index b97adae..548606f 100644
--- a/commands/instance/search.go
+++ b/commands/instance/search.go
@@ -20,6 +20,8 @@
import (
"regexp"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/metadata"
"github.com/urfave/cli"
@@ -62,6 +64,6 @@
}
}
}
- return display.Display(ctx, result)
+ return display.Display(ctx, &displayable.Displayable{Data: result})
},
}
diff --git a/commands/metrics/aggregation/topn.go b/commands/metrics/aggregation/topn.go
index 77876f6..ce6c63d 100644
--- a/commands/metrics/aggregation/topn.go
+++ b/commands/metrics/aggregation/topn.go
@@ -22,6 +22,8 @@
"strconv"
"strings"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/commands/interceptor"
"github.com/urfave/cli"
@@ -107,6 +109,6 @@
metricsValues = aggregation.ServiceTopN(ctx, name, topN, duration, order)
}
- return display.Display(ctx, metricsValues)
+ return display.Display(ctx, &displayable.Displayable{Data: metricsValues})
},
}
diff --git a/commands/metrics/linear/linear-metrics.go b/commands/metrics/linear/linear-metrics.go
index d435063..c1aa19b 100644
--- a/commands/metrics/linear/linear-metrics.go
+++ b/commands/metrics/linear/linear-metrics.go
@@ -20,6 +20,8 @@
import (
"github.com/urfave/cli"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/metrics"
"github.com/apache/skywalking-cli/graphql/utils"
@@ -75,6 +77,6 @@
ID: id,
}, duration)
- return display.Display(ctx, utils.MetricsToMap(duration, metricsValues))
+ return display.Display(ctx, &displayable.Displayable{Data: utils.MetricsToMap(duration, metricsValues)})
},
}
diff --git a/commands/metrics/linear/multiple-linear-metrics.go b/commands/metrics/linear/multiple-linear-metrics.go
index 31db251..fae3580 100644
--- a/commands/metrics/linear/multiple-linear-metrics.go
+++ b/commands/metrics/linear/multiple-linear-metrics.go
@@ -20,6 +20,8 @@
import (
"github.com/urfave/cli"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/metrics"
"github.com/apache/skywalking-cli/graphql/utils"
@@ -88,6 +90,6 @@
reshaped[index] = utils.MetricsToMap(duration, value)
}
- return display.Display(ctx, reshaped)
+ return display.Display(ctx, &displayable.Displayable{Data: reshaped})
},
}
diff --git a/commands/metrics/single/single-metrics.go b/commands/metrics/single/single-metrics.go
index 69405ea..1f710c0 100644
--- a/commands/metrics/single/single-metrics.go
+++ b/commands/metrics/single/single-metrics.go
@@ -20,6 +20,8 @@
import (
"strings"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/metrics"
"github.com/urfave/cli"
@@ -75,6 +77,6 @@
Step: step.(*model.StepEnumValue).Selected,
})
- return display.Display(ctx, metricsValues.Values)
+ return display.Display(ctx, &displayable.Displayable{Data: metricsValues.Values})
},
}
diff --git a/commands/metrics/thermodynamic/thermodynamic.go b/commands/metrics/thermodynamic/thermodynamic.go
index 5eefb3a..05c4ba4 100644
--- a/commands/metrics/thermodynamic/thermodynamic.go
+++ b/commands/metrics/thermodynamic/thermodynamic.go
@@ -20,6 +20,8 @@
import (
"github.com/urfave/cli"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/commands/flags"
"github.com/apache/skywalking-cli/commands/interceptor"
"github.com/apache/skywalking-cli/commands/model"
@@ -62,6 +64,10 @@
Name: metricsName,
}, duration)
- return display.Display(ctx, metricsValues)
+ return display.Display(ctx, &displayable.Displayable{
+ Data: metricsValues,
+ Duration: duration,
+ Title: metricsName,
+ })
},
}
diff --git a/commands/service/list.go b/commands/service/list.go
index 2604a6f..14e2830 100644
--- a/commands/service/list.go
+++ b/commands/service/list.go
@@ -20,6 +20,8 @@
import (
"github.com/urfave/cli"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/metadata"
"github.com/apache/skywalking-cli/commands/flags"
@@ -58,6 +60,6 @@
services = []schema.Service{service}
}
- return display.Display(ctx, services)
+ return display.Display(ctx, &displayable.Displayable{Data: services})
},
}
diff --git a/display/display.go b/display/display.go
index e620673..af3a598 100644
--- a/display/display.go
+++ b/display/display.go
@@ -21,6 +21,8 @@
"fmt"
"strings"
+ d "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/display/graph"
"github.com/urfave/cli"
@@ -38,18 +40,18 @@
)
// Display the object in the style specified in flag --display
-func Display(ctx *cli.Context, object interface{}) error {
+func Display(ctx *cli.Context, displayable *d.Displayable) error {
displayStyle := ctx.GlobalString("display")
switch strings.ToLower(displayStyle) {
case JSON:
- return json.Display(object)
+ return json.Display(displayable)
case YAML:
- return yaml.Display(object)
+ return yaml.Display(displayable)
case TABLE:
- return table.Display(object)
+ return table.Display(displayable)
case GRAPH:
- return graph.Display(object)
+ return graph.Display(displayable)
default:
return fmt.Errorf("unsupported display style: %s", displayStyle)
}
diff --git a/display/displayable/displayable.go b/display/displayable/displayable.go
new file mode 100644
index 0000000..12ca7d2
--- /dev/null
+++ b/display/displayable/displayable.go
@@ -0,0 +1,26 @@
+// 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 displayable
+
+import "github.com/apache/skywalking-cli/graphql/schema"
+
+type Displayable struct {
+ Data interface{}
+ Duration schema.Duration
+ Title string
+}
diff --git a/display/graph/graph.go b/display/graph/graph.go
index c27f835..e8fc072 100644
--- a/display/graph/graph.go
+++ b/display/graph/graph.go
@@ -21,21 +21,31 @@
"fmt"
"reflect"
+ "github.com/apache/skywalking-cli/display/graph/heatmap"
+ "github.com/apache/skywalking-cli/graphql/schema"
+
+ d "github.com/apache/skywalking-cli/display/displayable"
"github.com/apache/skywalking-cli/display/graph/linear"
)
-func Display(object interface{}) error {
- if reflect.TypeOf(object) == reflect.TypeOf(map[string]float64{}) {
- kvs := []map[string]float64{object.(map[string]float64)}
+func Display(displayable *d.Displayable) error {
+ data := displayable.Data
+
+ if reflect.TypeOf(data) == reflect.TypeOf(schema.Thermodynamic{}) {
+ return heatmap.Display(displayable)
+ }
+
+ if reflect.TypeOf(data) == reflect.TypeOf(map[string]float64{}) {
+ kvs := []map[string]float64{data.(map[string]float64)}
return linear.Display(kvs)
}
- if reflect.TypeOf(object) == reflect.TypeOf([]map[string]float64{}) {
- kvs := object.([]map[string]float64)
+ if reflect.TypeOf(data) == reflect.TypeOf([]map[string]float64{}) {
+ kvs := data.([]map[string]float64)
return linear.Display(kvs)
}
- return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(object))
+ return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(data))
}
diff --git a/display/graph/heatmap/heatmap.go b/display/graph/heatmap/heatmap.go
new file mode 100644
index 0000000..0ca653a
--- /dev/null
+++ b/display/graph/heatmap/heatmap.go
@@ -0,0 +1,114 @@
+// 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 heatmap
+
+import (
+ "fmt"
+ "math"
+ "time"
+
+ "github.com/apache/skywalking-cli/graphql/utils"
+ "github.com/apache/skywalking-cli/util"
+
+ ui "github.com/gizak/termui/v3"
+
+ d "github.com/apache/skywalking-cli/display/displayable"
+ "github.com/apache/skywalking-cli/graphql/schema"
+ "github.com/apache/skywalking-cli/lib"
+)
+
+func Display(displayable *d.Displayable) error {
+ data := displayable.Data.(schema.Thermodynamic)
+
+ nodes := data.Nodes
+ duration := displayable.Duration
+
+ rows, cols, min, max := statistics(nodes)
+
+ if err := ui.Init(); err != nil {
+ return err
+ }
+ defer ui.Close()
+
+ termW, _ := ui.TerminalDimensions()
+
+ 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)
+
+ if err != nil {
+ return err
+ }
+
+ hm.XLabels[i] = startTime.Add(time.Duration(i) * step).Format("15:04")
+ }
+ for i := 0; i < cols; i++ {
+ hm.YLabels[i] = fmt.Sprintf("%4d", i*data.AxisYStep)
+ }
+
+ 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)
+ }
+
+ 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}
+ }
+
+ 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 {
+ }
+ 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]))
+ }
+
+ rows++
+ cols++
+ return
+}
diff --git a/display/json/json.go b/display/json/json.go
index 25aaccf..12a506f 100644
--- a/display/json/json.go
+++ b/display/json/json.go
@@ -20,14 +20,15 @@
import (
"encoding/json"
"fmt"
+
+ d "github.com/apache/skywalking-cli/display/displayable"
)
-func Display(object interface{}) error {
- if bytes, e := json.Marshal(object); e == nil {
- fmt.Printf("%v\n", string(bytes))
- } else {
+func Display(displayable *d.Displayable) error {
+ bytes, e := json.Marshal(displayable.Data)
+ if e != nil {
return e
}
-
- return nil
+ _, e = fmt.Printf("%v\n", string(bytes))
+ return e
}
diff --git a/display/json/json_test.go b/display/json/json_test.go
index 8e30107..9d8ead4 100644
--- a/display/json/json_test.go
+++ b/display/json/json_test.go
@@ -20,6 +20,8 @@
import (
"testing"
+ d "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/schema"
)
@@ -36,7 +38,7 @@
}
func display(t *testing.T, result []schema.Service) {
- if err := Display(result); err != nil {
+ if err := Display(&d.Displayable{Data: result}); err != nil {
t.Error(err)
}
}
diff --git a/display/table/table.go b/display/table/table.go
index ce2a5ea..8e2227d 100644
--- a/display/table/table.go
+++ b/display/table/table.go
@@ -21,15 +21,17 @@
"encoding/json"
"os"
+ d "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/logger"
"github.com/olekukonko/tablewriter"
)
-func Display(object interface{}) error {
+func Display(displayable *d.Displayable) error {
var stringMapArrays []map[string]string
- bytes, _ := json.Marshal(object)
+ bytes, _ := json.Marshal(displayable.Data)
_ = json.Unmarshal(bytes, &stringMapArrays)
if len(stringMapArrays) < 1 {
diff --git a/display/table/table_test.go b/display/table/table_test.go
index fd24460..ec2b265 100644
--- a/display/table/table_test.go
+++ b/display/table/table_test.go
@@ -20,6 +20,8 @@
import (
"testing"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/schema"
)
@@ -36,7 +38,7 @@
}
func display(t *testing.T, result []schema.Service) {
- if err := Display(result); err != nil {
+ if err := Display(&displayable.Displayable{Data: result}); err != nil {
t.Error(err)
}
}
diff --git a/display/yaml/yaml.go b/display/yaml/yaml.go
index bb8cb79..3f3c98b 100644
--- a/display/yaml/yaml.go
+++ b/display/yaml/yaml.go
@@ -20,15 +20,16 @@
import (
"fmt"
+ d "github.com/apache/skywalking-cli/display/displayable"
+
"gopkg.in/yaml.v2"
)
-func Display(object interface{}) error {
- if bytes, e := yaml.Marshal(object); e == nil {
- fmt.Printf("%v", string(bytes))
- } else {
+func Display(displayable *d.Displayable) error {
+ bytes, e := yaml.Marshal(displayable.Data)
+ if e != nil {
return e
}
-
- return nil
+ _, e = fmt.Printf("%v", string(bytes))
+ return e
}
diff --git a/display/yaml/yaml_test.go b/display/yaml/yaml_test.go
index 81a772b..aaada3a 100644
--- a/display/yaml/yaml_test.go
+++ b/display/yaml/yaml_test.go
@@ -20,6 +20,8 @@
import (
"testing"
+ "github.com/apache/skywalking-cli/display/displayable"
+
"github.com/apache/skywalking-cli/graphql/schema"
)
@@ -36,7 +38,7 @@
}
func display(t *testing.T, result []schema.Service) {
- if err := Display(result); err != nil {
+ if err := Display(&displayable.Displayable{Data: result}); err != nil {
t.Error(err)
}
}
diff --git a/dist/LICENSE b/dist/LICENSE
index e5b6732..4dfc4c4 100644
--- a/dist/LICENSE
+++ b/dist/LICENSE
@@ -222,6 +222,8 @@
sirupsen (logrus) 1.4.2: https://github.com/sirupsen/logrus MIT
urfave (cli) 1.22.1: https://github.com/urfave/cli MIT
nsf (termbox-go) 0.0.0-20190817171036-93860e161317: https://github.com/nsf/termbox-go MIT
+ gizak (termui) v3: https://github.com/gizak/termui MIT
+ mattn (go-runewidth) v3: https://github.com/mattn/go-runewidth MIT
========================================================================
BSD licenses
diff --git a/dist/licenses/LICENSE-go-runewidth b/dist/licenses/LICENSE-go-runewidth
new file mode 100644
index 0000000..91b5cef
--- /dev/null
+++ b/dist/licenses/LICENSE-go-runewidth
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Yasuhiro Matsumoto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/dist/licenses/LICENSE-termui b/dist/licenses/LICENSE-termui
new file mode 100644
index 0000000..b8beeb7
--- /dev/null
+++ b/dist/licenses/LICENSE-termui
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Zack Guo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/go.mod b/go.mod
index f28ffcc..7586d33 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,11 @@
require (
github.com/99designs/gqlgen v0.11.1 // indirect
+ github.com/gizak/termui/v3 v3.1.0
github.com/machinebox/graphql v0.2.2
+ github.com/mattn/go-runewidth v0.0.4
github.com/mum4k/termdash v0.10.0
- github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 // indirect
+ github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317
github.com/olekukonko/tablewriter v0.0.2
github.com/sirupsen/logrus v1.4.2
github.com/urfave/cli v1.22.1
diff --git a/go.sum b/go.sum
index cd718b5..932479d 100644
--- a/go.sum
+++ b/go.sum
@@ -12,6 +12,9 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
+github.com/gizak/termui v3.1.0+incompatible h1:N3CFm+j087lanTxPpHOmQs0uS3s5I9TxoAFy6DqPqv8=
+github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
+github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -34,12 +37,16 @@
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mum4k/termdash v0.10.0 h1:uqM6ePiMf+smecb1tJJeON36o1hREeCfOmLFG0iz4a0=
github.com/mum4k/termdash v0.10.0/go.mod h1:l3tO+lJi9LZqXRq7cu7h5/8rDIK3AzelSuq2v/KncxI=
+github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 h1:hhGN4SFXgXo61Q4Sjj/X9sBjyeSa2kdpaOzCO+8EVQw=
github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
diff --git a/graphql/metrics/metrics.go b/graphql/metrics/metrics.go
index 68cf36d..ef71e32 100644
--- a/graphql/metrics/metrics.go
+++ b/graphql/metrics/metrics.go
@@ -85,7 +85,7 @@
request := graphql.NewRequest(`
query ($metric: MetricCondition!, $duration: Duration!) {
metrics: getThermodynamic(metric: $metric, duration: $duration) {
- nodes responseTimeStep: axisYStep
+ nodes axisYStep
}
}
`)
diff --git a/lib/heatmap.go b/lib/heatmap.go
new file mode 100644
index 0000000..bf24b86
--- /dev/null
+++ b/lib/heatmap.go
@@ -0,0 +1,114 @@
+// 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)]
+}
diff --git a/util/math.go b/util/math.go
new file mode 100644
index 0000000..ddbe2a8
--- /dev/null
+++ b/util/math.go
@@ -0,0 +1,25 @@
+// 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 util
+
+func MaxInt(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}