diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | README.md | 93 | ||||
-rw-r--r-- | cmd/gotop/main.go | 23 | ||||
-rw-r--r-- | config.go | 3 | ||||
-rw-r--r-- | go.sum | 12 | ||||
-rw-r--r-- | layout/layout.go | 54 | ||||
-rw-r--r-- | layout/layout_test.go | 27 | ||||
-rw-r--r-- | layouts/kitchensink | 4 | ||||
-rw-r--r-- | layouts/many_columns_test | 4 | ||||
-rw-r--r-- | termui/gauge.go | 22 | ||||
-rw-r--r-- | widgets/battery.go | 27 | ||||
-rw-r--r-- | widgets/batterygauge.go | 77 | ||||
-rw-r--r-- | widgets/cpu.go | 42 | ||||
-rw-r--r-- | widgets/disk.go | 24 | ||||
-rw-r--r-- | widgets/mem.go | 30 | ||||
-rw-r--r-- | widgets/mem_freebsd.go | 3 | ||||
-rw-r--r-- | widgets/mem_other.go | 3 | ||||
-rw-r--r-- | widgets/net.go | 23 | ||||
-rw-r--r-- | widgets/proc.go | 4 | ||||
-rw-r--r-- | widgets/temp.go | 19 |
20 files changed, 439 insertions, 56 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 521e9b8..f4468be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.3.2] - ?? +- Fixes #15, crash caused by battery widget when some accessories have batteries - Fixes #57, colors with dashes in the name not found. - Also, cjbassi/gotop#127 and cjbassi/gotop#130 were released back in v3.1.0. @@ -111,12 +111,12 @@ and these are separated by spaces. 1. Each line is a row 2. Empty lines are skipped 3. Spaces are compressed (so you can do limited visual formatting) -4. Legal widget names are: cpu, disk, mem, temp, batt, net, procs +4. Legal widget names are: cpu, disk, mem, temp, batt, net, procs, power 5. Widget names are not case sensitive 4. The simplest row is a single widget, by name, e.g. - ``` - cpu - ``` + ``` + cpu + ``` 5. **Weights** 1. Widgets with no weights have a weight of 1. 2. If multiple widgets are put on a row with no weights, they will all have @@ -134,19 +134,19 @@ and these are separated by spaces. second will be 5/7 ~= 67% wide (or, memory will be twice as wide as disk). 9. If prefixed by a number and colon, the widget will span that number of rows downward. E.g. - ``` - mem 2:cpu - net - ``` + ``` + mem 2:cpu + net + ``` Here, memory and network will be in the same row as CPU, one over the other, and each half as high as CPU; it'll look like this: - ``` - +------+------+ - | Mem | | - +------+ CPU | - | Net | | - +------+------+ - ``` + ``` + +------+------+ + | Mem | | + +------+ CPU | + | Net | | + +------+------+ + ``` 10. Negative, 0, or non-integer weights will be recorded as "1". Same for row spans. 11. Unrecognized widget names will cause the application to abort. @@ -163,23 +163,71 @@ and these are separated by spaces. Yes, you're clever enough to break the layout algorithm, but if you try to build massive edifices, you're in for disappointment. +### Metrics + +gotop can export widget data as Prometheus metrics. This allows users to take +snapshots of the current state of a machine running gotop, or to query gotop +remotely. + +All metrics are in the `gotop` namespace, and are tagged with +`goto_<widget>_<enum>`. Metrics are only exported for widgets +that are enabled, and are updated with the same frequency as the configured +update interval. Most widgets are exported as Prometheus gauges. + +Metrics are disabled by default, and must be enabled with the `--export` flag. +The flag takes an interface port in the idiomatic Go format of +`<addy>:<port>`; a common pattern is `-x :2112`. There is **no security** +on this feature; I recommend that you run this bound to a localhost interface, +e.g. `127.0.0.1:7653`, and if you want to access this remotely, run it behind +a proxy that provides SSL and authentication such as +[Caddy](https://caddyserver.com). + +Once enabled, any widgets that are enabled will appear in the HTTP payload of +a call to `http://<addy>:<port>/metrics`. For example, + +``` +➜ ~ curl -s http://localhost:2112/metrics | egrep -e '^gotop' +gotop_battery_0 0.6387792286668692 +gotop_cpu_0 12.871287128721228 +gotop_cpu_1 11.000000000001364 +gotop_disk_:dev:nvme0n1p1 0.63 +gotop_memory_main 49.932259713701434 +gotop_memory_swap 0 +gotop_net_recv 129461 +gotop_net_sent 218525 +gotop_temp_coretemp_core0 37 +gotop_temp_coretemp_core1 37 +``` + +Disk metrics are reformatted to replace `/` with `:` which makes them legal +Prometheus names: + +``` +➜ ~ curl -s http://localhost:2112/metrics | egrep -e '^gotop_disk' | tr ':' '/' +gotop_disk_/dev/nvme0n1p1 0.63 +``` + +This feature satisfies a ticket request to provide a "snapshot" for comparison +with a known state, but it is also foundational for a future feature where +widgets can be configured with virtual devices fed by data from remote gotop +instances. The objective for that feature is to allow monitoring of multiple +remote VMs without having to have a wall of gotops running on a large monitor. + ### CLI Options `-c`, `--color=NAME` Set a colorscheme. -`-m`, `--minimal` Only show CPU, Mem and Process widgets. (DEPRECATED for `-l minimal`) +`-m`, `--minimal` Only show CPU, Mem and Process widgets. (DEPRECATED for `-l minimal`) `-r`, `--rate=RATE` Number of times per second to update CPU and Mem widgets [default: 1]. `-V`, `--version` Print version and exit. `-p`, `--percpu` Show each CPU in the CPU widget. `-a`, `--averagecpu` Show average CPU in the CPU widget. `-f`, `--fahrenheit` Show temperatures in fahrenheit. `-s`, `--statusbar` Show a statusbar with the time. -`-b`, `--battery` Show battery level widget (`minimal` turns off). [preview](./assets/screenshots/battery.png) (DEPRECATED for `-l battery`) -`-i`, `--interface=NAME` Select network interface [default: all]. -`-l`, `--layout=NAME` Choose a layout. gotop searches for a file by NAME in \$XDG_CONFIG_HOME/gotop, then relative to the current path. "-" reads a layout from stdin, allowing for simple, one-off layouts such as `echo net | gotop -l -` - -Several interfaces can be defined using comma separated values. +`-b`, `--battery` Show battery level widget (`minimal` turns off). [preview](./assets/screenshots/battery.png) (DEPRECATED for `-l battery`) +`-i`, `--interface=NAME` Select network interface. Several interfaces can be defined using comma separated values. Interfaces can also be ignored by prefixing the interface with `!` [default: all]. +`-l`, `--layout=NAME` Choose a layout. gotop searches for a file by NAME in \$XDG_CONFIG_HOME/gotop, then relative to the current path. "-" reads a layout from stdin, allowing for simple, one-off layouts such as `echo net | gotop -l -` +`-x`, `--export=PORT` Enable metrics for export on the specified port. This feature is disabled by default. -Interfaces can also be ignored using `!` ## Built With @@ -189,3 +237,4 @@ Interfaces can also be ignored using `!` - [shirou/gopsutil](https://github.com/shirou/gopsutil) - [goreleaser/nfpm](https://github.com/goreleaser/nfpm) - [distatus/battery](https://github.com/distatus/battery) +- [prometheus/client_golang](https://github.com/prometheus/client_golang) diff --git a/cmd/gotop/main.go b/cmd/gotop/main.go index 7575655..c708b39 100644 --- a/cmd/gotop/main.go +++ b/cmd/gotop/main.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "log" + "net/http" "os" "os/signal" "path/filepath" @@ -14,6 +15,7 @@ import ( docopt "github.com/docopt/docopt.go" ui "github.com/gizak/termui/v3" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/xxxserxxx/gotop" "github.com/xxxserxxx/gotop/colorschemes" @@ -25,7 +27,7 @@ import ( const ( appName = "gotop" - version = "3.3.1" + version = "3.4.0" graphHorizontalScaleDelta = 3 defaultUI = "cpu\ndisk/1 2:mem/2\ntemp\nnet procs" @@ -41,6 +43,7 @@ var ( stderrLogger = log.New(os.Stderr, "", 0) ) +// TODO: Add tab completion for Linux https://gist.github.com/icholy/5314423 // TODO: state:merge #135 linux console font (cmatsuoka/console-font) // TODO: state:deferred 157 FreeBSD fixes & Nvidia GPU support (kraust/master). Significant CPU use impact for NVidia changes. // TODO: Virtual devices from Prometeus metrics @feature @@ -64,6 +67,7 @@ Options: -B, --bandwidth=bits Specify the number of bits per seconds. -l, --layout=NAME Name of layout spec file for the UI. Looks first in $XDG_CONFIG_HOME/gotop, then as a path. Use "-" to pipe. -i, --interface=NAME Select network interface [default: all]. + -x, --export=PORT Enable metrics for export on the specified port. Several interfaces can be defined using comma separated values. @@ -115,8 +119,11 @@ Colorschemes: if args["--minimal"].(bool) { conf.Layout = "minimal" } - if val, _ := args["--statusbar"]; val != nil { - rateStr, _ := args["--rate"].(string) + if val, _ := args["--export"]; val != nil { + conf.ExportPort = val.(string) + } + if val, _ := args["--rate"]; val != nil { + rateStr, _ := val.(string) rate, err := strconv.ParseFloat(rateStr, 64) if err != nil { return fmt.Errorf("invalid rate parameter") @@ -335,7 +342,7 @@ func makeConfig() gotop.Config { HelpVisible: false, UpdateInterval: time.Second, AverageLoad: false, - PercpuLoad: false, + PercpuLoad: true, TempScale: w.Celsius, Statusbar: false, NetInterface: w.NET_INTERFACE_ALL, @@ -345,6 +352,7 @@ func makeConfig() gotop.Config { return conf } +// TODO: mpd visualizer widget func main() { // Set up default config conf := makeConfig() @@ -400,6 +408,12 @@ func main() { ui.Render(bar) } + if conf.ExportPort != "" { + go func() { + http.Handle("/metrics", promhttp.Handler()) + http.ListenAndServe(conf.ExportPort, nil) + }() + } eventLoop(conf, grid) } @@ -414,7 +428,6 @@ func getLayout(conf gotop.Config) io.Reader { case "battery": return strings.NewReader(batteryUI) default: - log.Printf("layout = %s", conf.Layout) fp := filepath.Join(conf.ConfigDir, conf.Layout) fin, err := os.Open(fp) if err != nil { @@ -30,6 +30,7 @@ type Config struct { NetInterface string Layout string MaxLogSize int64 + ExportPort string } func Parse(in io.Reader, conf *Config) error { @@ -109,6 +110,8 @@ func Parse(in io.Reader, conf *Config) error { return err } conf.MaxLogSize = int64(iv) + case "export": + conf.ExportPort = kv[1] } } @@ -14,15 +14,11 @@ github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cjbassi/drawille-go v0.0.0-20190126131713-27dc511fe6fd h1:XtfPmj9tQRilnrEmI1HjQhxXWRhEM+m8CACtaMJE/kM= github.com/cjbassi/drawille-go v0.0.0-20190126131713-27dc511fe6fd/go.mod h1:vjcQJUZJYD3MeVGhtZXSMnCHfUNZxsyYzJt90eCYxK4= -github.com/cjbassi/drawille-go v0.1.0/go.mod h1:vjcQJUZJYD3MeVGhtZXSMnCHfUNZxsyYzJt90eCYxK4= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distatus/battery v0.9.0 h1:8NS5o00/j3Oh2xgocA6pQROTp5guoR+s8CZlWzHC4QM= github.com/distatus/battery v0.9.0/go.mod h1:gGO7GxHTi1zlRT+cAj8uGG0/8HFiqAeH0TJvoipnuPs= -github.com/distatus/battery v0.10.0 h1:YbizvmV33mqqC1fPCAEaQGV3bBhfYOfM+2XmL+mvt5o= -github.com/distatus/battery v0.10.0/go.mod h1:STnSvFLX//eEpkaN7qWRxCWxrWOcssTDgnG4yqq9BRE= github.com/docopt/docopt.go v0.0.0-20180111231733-ee0de3bc6815 h1:HMAfwOa33y82IaQEKQDfUCiwNlxtM1iw7HLM9ru0RNc= github.com/docopt/docopt.go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:l7JNRynTRuqe45tpIyItHNqZWTxywYjp87MWTOnU5cg= github.com/gizak/termui/v3 v3.0.0 h1:NYTUG6ig/sJK05O5FyhWemwlVPO8ilNpvS/PgRtrKAE= @@ -55,9 +51,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= 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= @@ -99,9 +93,6 @@ github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLk github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/shirou/gopsutil v2.18.11+incompatible h1:PMFTKnFTr/YTRW5rbLK4vWALV3a+IGXse5nvhSjztmg= github.com/shirou/gopsutil v2.18.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil v2.20.1+incompatible h1:oIq9Cq4i84Hk8uQAUOG3eNdI/29hBawGrD5YRl6JRDY= -github.com/shirou/gopsutil v2.20.1+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -112,7 +103,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 h1:Pn8fQdvx+z1avAi7fdM2kRYWQNxGlavNDSyzrQg2SsU= golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -135,11 +125,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= diff --git a/layout/layout.go b/layout/layout.go index 3214b97..0ab6b54 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -31,17 +31,17 @@ var widgetNames []string = []string{"cpu", "disk", "mem", "temp", "net", "procs" func Layout(wl layout, c gotop.Config) (*MyGrid, error) { rowDefs := wl.Rows - uiRows := make([]ui.GridItem, 0) + uiRows := make([][]interface{}, 0) numRows := countNumRows(wl.Rows) - var uiRow ui.GridItem + var uiRow []interface{} for len(rowDefs) > 0 { uiRow, rowDefs = processRow(c, numRows, rowDefs) uiRows = append(uiRows, uiRow) } rgs := make([]interface{}, 0) + rh := 1.0 / float64(len(uiRows)) for _, ur := range uiRows { - ur.HeightRatio = ur.HeightRatio / float64(numRows) - rgs = append(rgs, ur) + rgs = append(rgs, ui.NewRow(rh, ur...)) } grid := &MyGrid{ui.NewGrid(), nil, nil} grid.Set(rgs...) @@ -58,10 +58,10 @@ func Layout(wl layout, c gotop.Config) (*MyGrid, error) { // if there's a row span widget in the row; in this case, it'll consume as many // rows as the largest row span object in the row, and produce an uber-row // containing all that stuff. It returns a slice without the consumed elements. -func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridItem, [][]widgetRule) { +func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) ([]interface{}, [][]widgetRule) { // Recursive function #3. See the comment in deepFindProc. if len(rowDefs) < 1 { - return ui.GridItem{}, [][]widgetRule{} + return nil, [][]widgetRule{} } // The height of the tallest widget in this row; the number of rows that // will be consumed, and the overall height of the row that will be @@ -86,6 +86,7 @@ func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridIte columns = append(columns, make([]interface{}, 0)) } colHeights := make([]int, numCols) +outer: for i, row := range processing { // A definition may fill up the columns before all rows are consumed, // e.g. wid1/2 wid2/2. This block checks for that and, if it occurs, @@ -101,16 +102,25 @@ func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridIte rowDefs = append(processing[i:], rowDefs...) break } - // Not all rows have been consumed, so go ahead and place the row's widgets in columns - for _, wid := range row { - for j, ch := range colHeights { - if ch+wid.Height <= maxHeight { - widget := makeWidget(c, wid) - columns[j] = append(columns[j], ui.NewRow(float64(wid.Height)/float64(maxHeight), widget)) - colHeights[j] += wid.Height + // Not all rows have been consumed, so go ahead and place the row's + // widgets in columns + for w, widg := range row { + placed := false + for k := w; k < len(colHeights); k++ { // there are enough columns + ch := colHeights[k] + if ch+widg.Height <= maxHeight { + widget := makeWidget(c, widg) + columns[k] = append(columns[k], ui.NewRow(float64(widg.Height)/float64(maxHeight), widget)) + colHeights[k] += widg.Height + placed = true break } } + // If all columns are full, break out, return the row, and continue processing + if !placed { + rowDefs = processing[i:] + break outer + } } } var uiColumns []interface{} @@ -120,11 +130,15 @@ func processRow(c gotop.Config, numRows int, rowDefs [][]widgetRule) (ui.GridIte } } - return ui.NewRow(1.0/float64(numRows), uiColumns...), rowDefs + return uiColumns, rowDefs +} + +type Metric interface { + EnableMetric() } func makeWidget(c gotop.Config, widRule widgetRule) interface{} { - var w interface{} + var w Metric switch widRule.Widget { case "cpu": cpu := widgets.NewCpuWidget(c.UpdateInterval, c.GraphHorizontalScale, c.AverageLoad, c.PercpuLoad) @@ -145,7 +159,8 @@ func makeWidget(c gotop.Config, widRule widgetRule) interface{} { } w = cpu case "disk": - w = widgets.NewDiskWidget() + dw := widgets.NewDiskWidget() + w = dw case "mem": m := widgets.NewMemWidget(c.UpdateInterval, c.GraphHorizontalScale) m.LineColors["Main"] = ui.Color(c.Colorscheme.MainMem) @@ -185,10 +200,17 @@ func makeWidget(c gotop.Config, widRule widgetRule) interface{} { i++ } w = b + case "power": + b := widgets.NewBatteryGauge() + b.BarColor = ui.Color(c.Colorscheme.ProcCursor) + w = b default: log.Printf("Invalid widget name %s. Must be one of %v", widRule.Widget, widgetNames) return ui.NewBlock() } + if c.ExportPort != "" { + w.EnableMetric() + } return w } diff --git a/layout/layout_test.go b/layout/layout_test.go index d1f016d..568bbee 100644 --- a/layout/layout_test.go +++ b/layout/layout_test.go @@ -101,6 +101,33 @@ func TestParsing(t *testing.T) { assert.Equal(t, 1, l.Rows[1][0].Height) assert.Equal(t, 1.0, l.Rows[1][0].Weight) }}, + {"cpu/2 mem/1 6:procs\n3:temp/1 2:disk/2\npower\nnet procs", func(l layout) { + assert.Equal(t, 4, len(l.Rows)) + // First row + assert.Equal(t, 3, len(l.Rows[0])) + assert.Equal(t, 1, l.Rows[0][0].Height) + assert.Equal(t, 0.5, l.Rows[0][0].Weight) + assert.Equal(t, 1, l.Rows[0][1].Height) + assert.Equal(t, 0.25, l.Rows[0][1].Weight) + assert.Equal(t, 6, l.Rows[0][2].Height) + assert.Equal(t, 0.25, l.Rows[0][2].Weight) + // Second row + assert.Equal(t, 2, len(l.Rows[1])) + assert.Equal(t, 3, l.Rows[1][0].Height) + assert.Equal(t, 1/3.0, l.Rows[1][0].Weight) + assert.Equal(t, 2, l.Rows[1][1].Height) + assert.Equal(t, 2/3.0, l.Rows[1][1].Weight) + // Third row + assert.Equal(t, 1, len(l.Rows[2])) + assert.Equal(t, 1, l.Rows[2][0].Height) + assert.Equal(t, 1.0, l.Rows[2][0].Weight) + // Fourth row + assert.Equal(t, 2, len(l.Rows[3])) + assert.Equal(t, 1, l.Rows[3][0].Height) + assert.Equal(t, 0.5, l.Rows[3][0].Weight) + assert.Equal(t, 1, l.Rows[3][1].Height) + assert.Equal(t, 0.5, l.Rows[3][1].Weight) + }}, } for _, tc := range tests { diff --git a/layouts/kitchensink b/layouts/kitchensink new file mode 100644 index 0000000..5e25894 --- /dev/null +++ b/layouts/kitchensink @@ -0,0 +1,4 @@ +cpu/2 mem/1 +3:temp/1 2:disk/2 + power +net procs diff --git a/layouts/many_columns_test b/layouts/many_columns_test new file mode 100644 index 0000000..f2d57e3 --- /dev/null +++ b/layouts/many_columns_test @@ -0,0 +1,4 @@ +cpu/2 mem/1 6:procs/2 +3:temp/1 2:disk/2 +power +net procs diff --git a/termui/gauge.go b/termui/gauge.go new file mode 100644 index 0000000..db9a9c0 --- /dev/null +++ b/termui/gauge.go @@ -0,0 +1,22 @@ +package termui + +import ( + . "github.com/gizak/termui/v3" + gizak "github.com/gizak/termui/v3/widgets" +) + +// LineGraph implements a line graph of data points. +type Gauge struct { + *gizak.Gauge +} + +func NewGauge() *Gauge { + return &Gauge{ + Gauge: gizak.NewGauge(), + } +} + +func (self *Gauge) Draw(buf *Buffer) { + self.Gauge.Draw(buf) + self.Gauge.SetRect(self.Min.X, self.Min.Y, self.Inner.Dx(), self.Inner.Dy()) +} diff --git a/widgets/battery.go b/widgets/battery.go index 00764c0..b47cac4 100644 --- a/widgets/battery.go +++ b/widgets/battery.go @@ -8,6 +8,7 @@ import ( "time" "github.com/distatus/battery" + "github.com/prometheus/client_golang/prometheus" ui "github.com/xxxserxxx/gotop/termui" ) @@ -15,6 +16,7 @@ import ( type BatteryWidget struct { *ui.LineGraph updateInterval time.Duration + metric []prometheus.Gauge } func NewBatteryWidget(horizontalScale int) *BatteryWidget { @@ -41,6 +43,25 @@ func NewBatteryWidget(horizontalScale int) *BatteryWidget { return self } +func (b *BatteryWidget) EnableMetric() { + bats, err := battery.GetAll() + if err != nil { + log.Printf("error setting up metrics: %v", err) + return + } + b.metric = make([]prometheus.Gauge, len(bats)) + for i, bat := range bats { + gauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "gotop", + Subsystem: "battery", + Name: fmt.Sprintf("%d", i), + }) + gauge.Set(bat.Current / bat.Full) + b.metric[i] = gauge + prometheus.MustRegister(gauge) + } +} + func makeId(i int) string { return "Batt" + strconv.Itoa(i) } @@ -74,8 +95,12 @@ func (self *BatteryWidget) update() { } for i, battery := range batteries { id := makeId(i) - percentFull := math.Abs(battery.Current/battery.Full) * 100.0 + perc := battery.Current / battery.Full + percentFull := math.Abs(perc) * 100.0 self.Data[id] = append(self.Data[id], percentFull) self.Labels[id] = fmt.Sprintf("%3.0f%% %.0f/%.0f", percentFull, math.Abs(battery.Current), math.Abs(battery.Full)) + if self.metric != nil { + self.metric[i].Set(perc) + } } } diff --git a/widgets/batterygauge.go b/widgets/batterygauge.go new file mode 100644 index 0000000..04b1434 --- /dev/null +++ b/widgets/batterygauge.go @@ -0,0 +1,77 @@ +package widgets + +import ( + "fmt" + "log" + //"math" + //"strconv" + "time" + + "github.com/distatus/battery" + "github.com/prometheus/client_golang/prometheus" + + . "github.com/xxxserxxx/gotop/termui" +) + +type BatteryGauge struct { + *Gauge + metric prometheus.Gauge +} + +func NewBatteryGauge() *BatteryGauge { + self := &BatteryGauge{Gauge: NewGauge()} + self.Title = " Power Level " + + self.update() + + go func() { + for range time.NewTicker(time.Second).C { + self.Lock() + self.update() + self.Unlock() + } + }() + + return self +} + +func (b *BatteryGauge) EnableMetric() { + bats, err := battery.GetAll() + if err != nil { + log.Printf("error setting up metrics: %v", err) + return + } + mx := 0.0 + cu := 0.0 + for _, bat := range bats { + mx += bat.Full + cu += bat.Current + gauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "gotop", + Subsystem: "battery", + Name: "total", + }) + gauge.Set(cu / mx) + b.metric = gauge + prometheus.MustRegister(gauge) + } +} + +func (self *BatteryGauge) update() { + bats, err := battery.GetAll() + if err != nil { + log.Printf("error setting up metrics: %v", err) + return + } + mx := 0.0 + cu := 0.0 + for _, bat := range bats { + mx += bat.Full + cu += bat.Current + } + self.Percent = int((cu / mx) * 100.0) + self.Label = fmt.Sprintf("%d%%", self.Percent) + if self.metric != nil { + self.metric.Set(cu / mx) + } +} diff --git a/widgets/cpu.go b/widgets/cpu.go index 8e8819c..37c4220 100644 --- a/widgets/cpu.go +++ b/widgets/cpu.go @@ -1,11 +1,13 @@ package widgets import ( + "context" "fmt" "log" "sync" "time" + "github.com/prometheus/client_golang/prometheus" psCpu "github.com/shirou/gopsutil/cpu" ui "github.com/xxxserxxx/gotop/termui" @@ -19,6 +21,7 @@ type CpuWidget struct { updateInterval time.Duration formatString string updateLock sync.Mutex + metric []prometheus.Gauge } func NewCpuWidget(updateInterval time.Duration, horizontalScale int, showAverageLoad bool, showPerCpuLoad bool) *CpuWidget { @@ -71,6 +74,35 @@ func NewCpuWidget(updateInterval time.Duration, horizontalScale int, showAverage return self } +func (self *CpuWidget) EnableMetric() { + if self.ShowAverageLoad { + self.metric = make([]prometheus.Gauge, 1) + self.metric[0] = prometheus.NewGauge(prometheus.GaugeOpts{ + Subsystem: "cpu", + Name: "avg", + }) + } else { + ctx, ccl := context.WithTimeout(context.Background(), time.Second*5) + defer ccl() + percents, err := psCpu.PercentWithContext(ctx, self.updateInterval, true) + if err != nil { + log.Printf("error setting up metrics: %v", err) + return + } + self.metric = make([]prometheus.Gauge, self.CpuCount) + for i, perc := range percents { + gauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "gotop", + Subsystem: "cpu", + Name: fmt.Sprintf("%d", i), + }) + gauge.Set(perc) + prometheus.MustRegister(gauge) + self.metric[i] = gauge + } + } +} + func (b *CpuWidget) Scale(i int) { b.LineGraph.HorizontalScale = i } @@ -88,6 +120,9 @@ func (self *CpuWidget) update() { defer self.updateLock.Unlock() self.Data["AVRG"] = append(self.Data["AVRG"], percent[0]) self.Labels["AVRG"] = fmt.Sprintf("%3.0f%%", percent[0]) + if self.metric != nil { + self.metric[0].Set(percent[0]) + } } }() } @@ -109,6 +144,13 @@ func (self *CpuWidget) update() { key := fmt.Sprintf(self.formatString, i) self.Data[key] = append(self.Data[key], percent) self.Labels[key] = fmt.Sprintf("%3.0f%%", percent) + if self.metric != nil { + if self.metric[i] == nil { + log.Printf("ERROR: not enough metrics %d", i) + } else { + self.metric[i].Set(percent) + } + } } } } diff --git a/widgets/disk.go b/widgets/disk.go index d20b078..d1da7a6 100644 --- a/widgets/disk.go +++ b/widgets/disk.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" psDisk "github.com/shirou/gopsutil/disk" ui "github.com/xxxserxxx/gotop/termui" @@ -28,6 +29,7 @@ type DiskWidget struct { *ui.Table updateInterval time.Duration Partitions map[string]*Partition + metric map[string]prometheus.Gauge } func NewDiskWidget() *DiskWidget { @@ -60,6 +62,21 @@ func NewDiskWidget() *DiskWidget { return self } +func (self *DiskWidget) EnableMetric() { + self.metric = make(map[string]prometheus.Gauge) + for key, part := range self.Partitions { + gauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "gotop", + Subsystem: "disk", + Name: strings.ReplaceAll(key, "/", ":"), + //Name: strings.Replace(strings.Replace(part.Device, "/dev/", "", -1), "mapper/", "", -1), + }) + gauge.Set(float64(part.UsedPercent) / 100.0) + prometheus.MustRegister(gauge) + self.metric[key] = gauge + } +} + func (self *DiskWidget) update() { partitions, err := psDisk.Partitions(false) if err != nil { @@ -158,5 +175,12 @@ func (self *DiskWidget) update() { self.Rows[i][3] = partition.Free self.Rows[i][4] = partition.BytesReadRecently self.Rows[i][5] = partition.BytesWrittenRecently + if self.metric != nil { + if self.metric[key] == nil { + log.Printf("ERROR: missing metric %s", key) + } else { + self.metric[key].Set(float64(partition.UsedPercent) / 100.0) + } + } } } diff --git a/widgets/mem.go b/widgets/mem.go index d5bd67d..1c85f3b 100644 --- a/widgets/mem.go +++ b/widgets/mem.go @@ -5,6 +5,7 @@ import ( "log" "time" + "github.com/prometheus/client_golang/prometheus" psMem "github.com/shirou/gopsutil/mem" ui "github.com/xxxserxxx/gotop/termui" @@ -14,6 +15,8 @@ import ( type MemWidget struct { *ui.LineGraph updateInterval time.Duration + mainMetric prometheus.Gauge + swapMetric prometheus.Gauge } type MemoryInfo struct { @@ -45,6 +48,9 @@ func (self *MemWidget) updateMainMemory() { Used: mainMemory.Used, UsedPercent: mainMemory.UsedPercent, }) + if self.mainMetric != nil { + self.mainMetric.Set(mainMemory.UsedPercent) + } } } @@ -73,6 +79,30 @@ fun |