├── 2020-06-27-midna-webstat.jpg
├── go.mod
├── 2020-06-27-scan2drive-scan-gokrazy-stat.jpg
├── go.sum
├── internal
├── must
│ └── must.go
├── thermal
│ └── thermal.go
├── sys
│ └── sys.go
├── statflag
│ └── statflag.go
├── net
│ └── net.go
├── disk
│ └── disk.go
├── mem
│ └── mem.go
└── cpu
│ └── cpu.go
├── .github
└── workflows
│ └── go.yml
├── statexp
└── statexp.go
├── .goreleaser.yml
├── README.md
├── LICENSE
├── cmd
├── gokr-webstat
│ ├── statustmpl.go
│ └── webstat.go
└── gokr-stat
│ └── stat.go
└── stat.go
/2020-06-27-midna-webstat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokrazy/stat/HEAD/2020-06-27-midna-webstat.jpg
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gokrazy/stat
2 |
3 | go 1.18
4 |
5 | require golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
6 |
--------------------------------------------------------------------------------
/2020-06-27-scan2drive-scan-gokrazy-stat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokrazy/stat/HEAD/2020-06-27-scan2drive-scan-gokrazy-stat.jpg
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
2 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
3 |
--------------------------------------------------------------------------------
/internal/must/must.go:
--------------------------------------------------------------------------------
1 | package must
2 |
3 | import "strconv"
4 |
5 | func Uint64(s string) uint64 {
6 | v, err := strconv.ParseUint(s, 10, 64)
7 | if err != nil {
8 | return 0
9 | }
10 | return v
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | testandinstall:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Ensure all files were formatted as per gofmt
17 | run: |
18 | [ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
19 |
20 | - name: run tests
21 | run: go test ./...
22 |
23 | - name: install binaries
24 | run: go install github.com/gokrazy/stat/cmd/...
25 |
--------------------------------------------------------------------------------
/statexp/statexp.go:
--------------------------------------------------------------------------------
1 | // Package statexp is an experimental API for the gokrazy/stat package.
2 | package statexp
3 |
4 | import (
5 | "github.com/gokrazy/stat"
6 | "github.com/gokrazy/stat/internal/cpu"
7 | "github.com/gokrazy/stat/internal/disk"
8 | "github.com/gokrazy/stat/internal/mem"
9 | "github.com/gokrazy/stat/internal/net"
10 | "github.com/gokrazy/stat/internal/sys"
11 | )
12 |
13 | type ProcessAndFormatter interface {
14 | ProcessAndFormat(map[string][]byte) []stat.Col
15 | }
16 |
17 | func DefaultModules() []ProcessAndFormatter {
18 | return []ProcessAndFormatter{
19 | &cpu.Stats{},
20 | &disk.Stats{},
21 | &sys.Stats{},
22 | &net.Stats{},
23 | &mem.Stats{},
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/internal/thermal/thermal.go:
--------------------------------------------------------------------------------
1 | package thermal
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gokrazy/stat"
7 | "github.com/gokrazy/stat/internal/must"
8 | )
9 |
10 | type reading struct {
11 | temp uint64
12 | }
13 |
14 | type Stats struct {
15 | cur reading
16 | }
17 |
18 | func (s *Stats) Headers() []string {
19 | return []string{"cpu"}
20 | }
21 |
22 | func (s *Stats) FileContents() []string {
23 | return []string{"/sys/class/thermal/thermal_zone0/temp"}
24 | }
25 |
26 | func (s *Stats) process(contents map[string][]byte) {
27 | s.cur = reading{}
28 |
29 | line := string(contents["/sys/class/thermal/thermal_zone0/temp"])
30 | if len(line) == 0 {
31 | return
32 | }
33 |
34 | therm := strings.TrimSpace(line)
35 | s.cur.temp = must.Uint64(therm)
36 | }
37 |
38 | func (s *Stats) ProcessAndFormat(contents map[string][]byte) []stat.Col {
39 | s.process(contents)
40 | return []stat.Col{
41 | {Type: stat.ColPercentage, ValFloat64: float64(s.cur.temp) / 1000, Width: 3, Scale: 34},
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # This is an example goreleaser.yaml file with some sane defaults.
2 | # Make sure to check the documentation at http://goreleaser.com
3 | before:
4 | hooks:
5 | - go mod download
6 | - go generate ./...
7 | builds:
8 | - id: "stat"
9 | main: "./cmd/gokr-stat"
10 | binary: "gokr-stat"
11 | env:
12 | - CGO_ENABLED=0
13 | goos:
14 | - linux
15 | goarch:
16 | - amd64
17 | - arm
18 | - arm64
19 | - 386
20 | - id: "webstat"
21 | main: "./cmd/gokr-webstat"
22 | binary: "gokr-webstat"
23 | env:
24 | - CGO_ENABLED=0
25 | goos:
26 | - linux
27 | goarch:
28 | - amd64
29 | - arm
30 | - arm64
31 | - 386
32 | archives:
33 | - name_template: "gokrazy_stat_{{ .Os }}_{{ .Arch }}"
34 | replacements:
35 | darwin: Darwin
36 | linux: Linux
37 | windows: Windows
38 | 386: i386
39 | amd64: x86_64
40 | checksum:
41 | name_template: 'checksums.txt'
42 | snapshot:
43 | name_template: "{{ .Tag }}-next"
44 | changelog:
45 | sort: asc
46 | filters:
47 | exclude:
48 | - '^docs:'
49 | - '^test:'
50 | release:
51 | github:
52 | owner: gokrazy
53 | name: stat
54 |
--------------------------------------------------------------------------------
/internal/sys/sys.go:
--------------------------------------------------------------------------------
1 | package sys
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gokrazy/stat"
7 | "github.com/gokrazy/stat/internal/must"
8 | )
9 |
10 | type reading struct {
11 | // TODO: verify in the kernel source the data type for these
12 | int uint64
13 | csw uint64
14 | }
15 |
16 | type Stats struct {
17 | old, cur reading
18 | }
19 |
20 | func (s *Stats) Headers() []string {
21 | return []string{" int ", " csw "}
22 | }
23 |
24 | func (s *Stats) FileContents() []string {
25 | return []string{"/proc/stat"}
26 | }
27 |
28 | func (s *Stats) process(contents map[string][]byte) {
29 | s.old = s.cur
30 | s.cur = reading{}
31 |
32 | lines := strings.Split(string(contents["/proc/stat"]), "\n")
33 | if len(lines) == 0 {
34 | return
35 | }
36 |
37 | for _, line := range lines {
38 | f := strings.Fields(line)
39 | if len(f) < 2 {
40 | continue
41 | }
42 | if f[0] == "intr" {
43 | s.cur.int = must.Uint64(f[1])
44 | }
45 | if f[0] == "ctxt" {
46 | s.cur.csw = must.Uint64(f[1])
47 | }
48 | }
49 | }
50 |
51 | func (s *Stats) ProcessAndFormat(contents map[string][]byte) []stat.Col {
52 | s.process(contents)
53 | return []stat.Col{
54 | stat.MetricCol(s.cur.int - s.old.int).WithWidth(5),
55 | stat.MetricCol(s.cur.csw - s.old.csw).WithWidth(5),
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/internal/statflag/statflag.go:
--------------------------------------------------------------------------------
1 | package statflag
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/gokrazy/stat"
8 | "github.com/gokrazy/stat/internal/cpu"
9 | "github.com/gokrazy/stat/internal/disk"
10 | "github.com/gokrazy/stat/internal/mem"
11 | "github.com/gokrazy/stat/internal/net"
12 | "github.com/gokrazy/stat/internal/sys"
13 | "github.com/gokrazy/stat/internal/thermal"
14 | )
15 |
16 | type Module interface {
17 | ProcessAndFormat(map[string][]byte) []stat.Col
18 | Headers() []string
19 | }
20 |
21 | func ModulesFromFlag(enabledModules string) ([]Module, error) {
22 | var modules []Module
23 | for _, name := range strings.Split(strings.TrimSpace(enabledModules), ",") {
24 | name = strings.TrimSpace(name)
25 | if name == "" {
26 | continue
27 | }
28 | switch name {
29 | case "cpu":
30 | modules = append(modules, &cpu.Stats{})
31 | case "disk":
32 | modules = append(modules, &disk.Stats{})
33 | case "sys":
34 | modules = append(modules, &sys.Stats{})
35 | case "net":
36 | modules = append(modules, &net.Stats{})
37 | case "mem":
38 | modules = append(modules, &mem.Stats{})
39 | case "thermal":
40 | modules = append(modules, &thermal.Stats{})
41 | default:
42 | return nil, fmt.Errorf("unknown module: %q", name)
43 | }
44 | }
45 | return modules, nil
46 | }
47 |
--------------------------------------------------------------------------------
/internal/net/net.go:
--------------------------------------------------------------------------------
1 | package net
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gokrazy/stat"
7 | "github.com/gokrazy/stat/internal/must"
8 | )
9 |
10 | type reading struct {
11 | recv uint64
12 | send uint64
13 | }
14 |
15 | type Stats struct {
16 | old, cur reading
17 | }
18 |
19 | func (s *Stats) Headers() []string {
20 | return []string{" recv", " send"}
21 | }
22 |
23 | func (s *Stats) FileContents() []string {
24 | return []string{"/proc/net/dev"}
25 | }
26 |
27 | func (s *Stats) process(contents map[string][]byte) {
28 | s.old = s.cur
29 | s.cur = reading{}
30 |
31 | lines := strings.Split(string(contents["/proc/net/dev"]), "\n")
32 | if len(lines) == 0 {
33 | return
34 | }
35 | var totalRecv uint64
36 | var totalSend uint64
37 | for _, line := range lines {
38 | line = strings.TrimSpace(strings.ReplaceAll(line, ":", ""))
39 |
40 | // As per proc(5):
41 | f := strings.Fields(line)
42 | if len(f) < 10 {
43 | continue
44 | }
45 | recv := must.Uint64(f[1])
46 | send := must.Uint64(f[9])
47 | totalRecv += recv
48 | totalSend += send
49 | }
50 |
51 | s.cur.recv = totalRecv
52 | s.cur.send = totalSend
53 | }
54 |
55 | func (s *Stats) ProcessAndFormat(contents map[string][]byte) []stat.Col {
56 | s.process(contents)
57 | return []stat.Col{
58 | stat.ByteCol(s.cur.recv - s.old.recv).WithWidth(5),
59 | stat.ByteCol(s.cur.send - s.old.send).WithWidth(5),
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gokrazy/stat
2 |
3 | [](https://goreportcard.com/report/github.com/gokrazy/stat)
4 |
5 | `gokrazy/stat` is a program to visualize resource usage, producing output that
6 | looks very similar to [Dag Wieers’s
7 | `dstat`](https://github.com/dstat-real/dstat) default output.
8 |
9 | As the repository path implies, this program is meant to be used on the
10 | https://gokrazy.org/ Raspberry Pi platform, so it does not have any external
11 | runtime dependencies and is implemented natively in Go (not using any cgo). The
12 | resulting static binary might come in handy in other Linux environments, too!
13 |
14 |
15 | ## github.com/gokrazy/stat/cmd/gokr-webstat
16 |
17 | `gokr-webstat` displays system resource usage in your browser, with output
18 | matching `gokr-stat` or `dstat`!
19 |
20 | It uses the [EventSource
21 | API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) to stream new
22 | lines into your browser.
23 |
24 | 
25 |
26 | ## github.com/gokrazy/stat/cmd/gokr-stat
27 |
28 | The `gokr-stat` program is a terminal program that displays system resource
29 | usage, like `dstat`.
30 |
31 | Here is the `gokr-stat` terminal output (via
32 | [`gokrazy/breakglass`](https://github.com/gokrazy/breakglass)) when scanning one
33 | double-sided piece of A4 paper with
34 | [scan2drive](https://github.com/stapelberg/scan2drive) on a Raspberry Pi 4:
35 |
36 | 
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 the gokrazy authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above
10 | copyright notice, this list of conditions and the following disclaimer
11 | in the documentation and/or other materials provided with the
12 | distribution.
13 | * Neither the name of gokrazy nor the names of its
14 | contributors may be used to endorse or promote products derived from
15 | this software without specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/cmd/gokr-webstat/statustmpl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | const statusTmpl = `
4 |
5 |
6 | {{ .Hostname }} - webstat
7 |
21 |
22 |
23 | {{ .Hostname }} - webstat
24 |
25 |
26 |
27 | {{ range $idx, $val := .Headers }}
28 | | {{ $val }} |
29 | {{ end }}
30 |
31 |
32 |
33 | | |
34 | | |
35 | | |
36 | | |
37 | | |
38 | | |
39 | | |
40 | | |
41 | | |
42 | | |
43 | | |
44 | | |
45 | | |
46 | | |
47 | | |
48 | | |
49 | | |
50 | | |
51 | | |
52 | | |
53 |
54 |
55 |
72 |
73 | `
74 |
--------------------------------------------------------------------------------
/internal/disk/disk.go:
--------------------------------------------------------------------------------
1 | package disk
2 |
3 | import (
4 | "regexp"
5 | "strings"
6 |
7 | "github.com/gokrazy/stat"
8 | "github.com/gokrazy/stat/internal/must"
9 | )
10 |
11 | type reading struct {
12 | read uint64
13 | write uint64
14 | }
15 |
16 | type Stats struct {
17 | old, cur reading
18 | }
19 |
20 | func (s *Stats) Headers() []string {
21 | return []string{" read", " writ"}
22 | }
23 |
24 | func (s *Stats) FileContents() []string {
25 | return []string{"/proc/diskstats"}
26 | }
27 |
28 | var diskfilterRe = regexp.MustCompile(`^([hsv]d[a-z]+\d+|cciss/c\d+d\d+p\d+|dm-\d+|md\d+|loop\d+p\d+|nvme\d+n\d+p\d+|mmcblk\d+p\d+|VxVM\d+)$`)
29 |
30 | func (s *Stats) process(contents map[string][]byte) {
31 | s.old = s.cur
32 | s.cur = reading{}
33 |
34 | lines := strings.Split(string(contents["/proc/diskstats"]), "\n")
35 | if len(lines) == 0 {
36 | return
37 | }
38 |
39 | var totalSectorsRead uint64
40 | var totalSectorsWritten uint64
41 | for _, line := range lines {
42 | line = strings.TrimSpace(line)
43 |
44 | // As per https://www.kernel.org/doc/Documentation/iostats.txt:
45 | f := strings.Fields(line)
46 | if len(f) < 10 {
47 | continue
48 | }
49 | if diskfilterRe.MatchString(f[2]) {
50 | // filters out e.g. dm-*
51 | continue
52 | }
53 |
54 | sectorsRead := must.Uint64(f[5])
55 | sectorsWritten := must.Uint64(f[9])
56 | totalSectorsRead += sectorsRead
57 | totalSectorsWritten += sectorsWritten
58 | }
59 |
60 | // TODO: reference linux kernel source if this is indeed hard-coded
61 | const sectorSize = 512
62 |
63 | s.cur.read = totalSectorsRead * sectorSize
64 | s.cur.write = totalSectorsWritten * sectorSize
65 |
66 | return
67 | }
68 |
69 | func (s *Stats) ProcessAndFormat(contents map[string][]byte) []stat.Col {
70 | s.process(contents)
71 | return []stat.Col{
72 | stat.ByteCol(s.cur.read - s.old.read).WithWidth(5),
73 | stat.ByteCol(s.cur.write - s.old.write).WithWidth(5),
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/internal/mem/mem.go:
--------------------------------------------------------------------------------
1 | package mem
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gokrazy/stat"
7 | "github.com/gokrazy/stat/internal/must"
8 | )
9 |
10 | type reading struct {
11 | used uint64
12 | free uint64
13 | buff uint64
14 | cach uint64
15 | }
16 |
17 | type Stats struct {
18 | old, cur reading
19 | }
20 |
21 | func (s *Stats) Headers() []string {
22 | return []string{" used", " free", " buff", " cach"}
23 | }
24 |
25 | func (s *Stats) FileContents() []string {
26 | return []string{"/proc/meminfo"}
27 | }
28 |
29 | func (s *Stats) process(contents map[string][]byte) {
30 | s.old = s.cur
31 | s.cur = reading{}
32 |
33 | lines := strings.Split(string(contents["/proc/meminfo"]), "\n")
34 | if len(lines) == 0 {
35 | return
36 | }
37 | var (
38 | memTotal uint64
39 | shmem uint64
40 | sreclaimable uint64
41 | )
42 | for _, line := range lines {
43 | line = strings.TrimSpace(strings.ReplaceAll(line, ":", ""))
44 |
45 | // As per proc(5):
46 | f := strings.Fields(line)
47 | if len(f) < 2 {
48 | continue
49 | }
50 | switch f[0] {
51 | case "MemFree":
52 | s.cur.free = must.Uint64(f[1]) * 1024
53 | case "Buffers":
54 | s.cur.buff = must.Uint64(f[1]) * 1024
55 | case "Cached":
56 | s.cur.cach = must.Uint64(f[1]) * 1024
57 | case "MemTotal":
58 | memTotal = must.Uint64(f[1]) * 1024
59 | case "Shmem":
60 | shmem = must.Uint64(f[1]) * 1024
61 | case "SReclaimable":
62 | sreclaimable = must.Uint64(f[1]) * 1024
63 | }
64 | }
65 |
66 | s.cur.used = memTotal - s.cur.free - s.cur.buff - s.cur.cach - sreclaimable + shmem
67 | }
68 |
69 | func (s *Stats) ProcessAndFormat(contents map[string][]byte) []stat.Col {
70 | s.process(contents)
71 | return []stat.Col{
72 | {Type: stat.ColGauge, Unit: stat.UnitBytesFloat, ValFloat64: float64(s.cur.used), Width: 5},
73 | {Type: stat.ColGauge, Unit: stat.UnitBytesFloat, ValFloat64: float64(s.cur.free), Width: 5},
74 | {Type: stat.ColGauge, Unit: stat.UnitBytesFloat, ValFloat64: float64(s.cur.buff), Width: 5},
75 | {Type: stat.ColGauge, Unit: stat.UnitBytesFloat, ValFloat64: float64(s.cur.cach), Width: 5},
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/internal/cpu/cpu.go:
--------------------------------------------------------------------------------
1 | package cpu
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gokrazy/stat"
7 | "github.com/gokrazy/stat/internal/must"
8 | )
9 |
10 | type reading struct {
11 | // TODO: verify in the kernel source the data type for these
12 | usr uint64 // user + nice + irq + softirq
13 | sys uint64 // system
14 | idl uint64 // idle
15 | wai uint64 // I/O wait
16 | stl uint64 // steal
17 |
18 | sum uint64
19 | }
20 |
21 | type Stats struct {
22 | old, cur reading
23 | }
24 |
25 | func (s *Stats) Headers() []string {
26 | return []string{"usr", "sys", "idl", "wai", "stl"}
27 | }
28 |
29 | func (s *Stats) FileContents() []string {
30 | return []string{"/proc/stat"}
31 | }
32 |
33 | func (s *Stats) process(contents map[string][]byte) {
34 | s.old = s.cur
35 | s.cur = reading{}
36 |
37 | lines := strings.Split(string(contents["/proc/stat"]), "\n")
38 | if len(lines) == 0 {
39 | return
40 | }
41 |
42 | // As per proc(5):
43 | f := strings.Fields(strings.TrimSpace(lines[0]))
44 | user := must.Uint64(f[1])
45 | nice := must.Uint64(f[2])
46 | system := must.Uint64(f[3])
47 | idle := must.Uint64(f[4])
48 | iowait := must.Uint64(f[5])
49 | irq := must.Uint64(f[6])
50 | softirq := must.Uint64(f[7])
51 | steal := must.Uint64(f[8])
52 |
53 | s.cur.usr = user + nice + irq + softirq
54 | s.cur.sys = system
55 | s.cur.idl = idle
56 | s.cur.wai = iowait
57 | s.cur.stl = steal
58 | s.cur.sum = s.cur.usr + s.cur.sys + s.cur.idl + s.cur.wai + s.cur.stl
59 | }
60 |
61 | func (s *Stats) ProcessAndFormat(contents map[string][]byte) []stat.Col {
62 | s.process(contents)
63 | total := float64(s.cur.sum - s.old.sum)
64 | return []stat.Col{
65 | {Type: stat.ColPercentage, ValFloat64: 100 * float64(s.cur.usr-s.old.usr) / total, Width: 3, Scale: 34},
66 | {Type: stat.ColPercentage, ValFloat64: 100 * float64(s.cur.sys-s.old.sys) / total, Width: 3, Scale: 34},
67 | {Type: stat.ColPercentage, ValFloat64: 100 * float64(s.cur.idl-s.old.idl) / total, Width: 3, Scale: 34},
68 | {Type: stat.ColPercentage, ValFloat64: 100 * float64(s.cur.wai-s.old.wai) / total, Width: 3, Scale: 34},
69 | {Type: stat.ColPercentage, ValFloat64: 100 * float64(s.cur.stl-s.old.stl) / total, Width: 3, Scale: 34},
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/cmd/gokr-stat/stat.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "log"
9 | "os"
10 | "os/signal"
11 | "strings"
12 | "sync"
13 | "syscall"
14 | "time"
15 | "unsafe"
16 |
17 | "github.com/gokrazy/stat"
18 | "github.com/gokrazy/stat/internal/statflag"
19 | )
20 |
21 | func formatCols(cols []stat.Col) string {
22 | formatted := make([]string, len(cols))
23 | for idx, col := range cols {
24 | formatted[idx] = col.String()
25 | }
26 | return strings.Join(formatted, " ")
27 | }
28 |
29 | const (
30 | TIOCGWINSZ = 0x5413
31 | )
32 |
33 | type window struct {
34 | Row uint16
35 | Col uint16
36 | Xpixel uint16
37 | Ypixel uint16
38 | }
39 |
40 | func terminalSize() (*window, error) {
41 | w := new(window)
42 | tio := syscall.TIOCGWINSZ
43 | res, _, err := syscall.Syscall(syscall.SYS_IOCTL,
44 | uintptr(syscall.Stdin),
45 | uintptr(tio),
46 | uintptr(unsafe.Pointer(w)),
47 | )
48 | if int(res) == -1 {
49 | if err == syscall.ENOTTY {
50 | return &window{Row: 80}, nil
51 | }
52 | return nil, err
53 | }
54 | return w, nil
55 | }
56 |
57 | func printStats() error {
58 | var enabledModules = flag.String("modules", "cpu,disk,sys,net,mem", "comma-separated list of modules to show. known modules: cpu,disk,sys,net,mem,thermal")
59 | flag.Parse()
60 |
61 | ts, err := terminalSize()
62 | if err != nil {
63 | return err
64 | }
65 | var rowsMu sync.Mutex
66 | rows := int(ts.Row) - 1
67 | ch := make(chan os.Signal, 1)
68 | signal.Notify(ch, syscall.SIGWINCH)
69 | go func() {
70 | for range ch {
71 | ts, err := terminalSize()
72 | if err != nil {
73 | log.Print(err)
74 | continue
75 | }
76 | rowsMu.Lock()
77 | rows = int(ts.Row) - 1
78 | rowsMu.Unlock()
79 | }
80 | }()
81 |
82 | modules, err := statflag.ModulesFromFlag(*enabledModules)
83 | if err != nil {
84 | return err
85 | }
86 |
87 | header := func() {
88 | const blue = "\033[1;34m"
89 | fmt.Printf("%s", blue)
90 | for idx, mod := range modules {
91 | headers := mod.Headers()
92 | suffix := " | "
93 | if idx == len(modules)-1 {
94 | suffix = "\n" // last module
95 | }
96 | fmt.Printf("%s%s", strings.Join(headers, " "), suffix)
97 | }
98 | }
99 |
100 | parts := make([]string, len(modules))
101 | files := make(map[string]*os.File)
102 | for _, mod := range modules {
103 | // When a stats module implements the FileContents() interface, we
104 | // ensure all returned file contents are read and passed to
105 | // ProcessAndFormat.
106 | fc, ok := mod.(interface{ FileContents() []string })
107 | if !ok {
108 | continue
109 | }
110 | for _, f := range fc.FileContents() {
111 | if _, ok := files[f]; ok {
112 | continue // already requested
113 | }
114 | fl, err := os.Open(f)
115 | if err != nil {
116 | return err
117 | }
118 | files[f] = fl
119 | }
120 | }
121 | for i := 0; ; i++ {
122 | rowsMu.Lock()
123 | showHeader := i%(int(rows)-1) == 0
124 | rowsMu.Unlock()
125 | if showHeader {
126 | header()
127 | }
128 | contents := make(map[string][]byte)
129 | for path, fl := range files {
130 | if _, err := fl.Seek(0, io.SeekStart); err != nil {
131 | return err
132 | }
133 | b, err := ioutil.ReadAll(fl)
134 | if err != nil {
135 | return err
136 | }
137 | contents[path] = b
138 | }
139 |
140 | for idx, mod := range modules {
141 | parts[idx] = formatCols(mod.ProcessAndFormat(contents))
142 | }
143 |
144 | if i > 0 {
145 | const darkblue = "\033[0;34m"
146 | fmt.Println(strings.Join(parts, darkblue+" | "))
147 | // TODO: clear colors at the end of line so that program can be interrupted
148 | }
149 |
150 | time.Sleep(1 * time.Second)
151 | }
152 | }
153 |
154 | func main() {
155 | if os.Getenv("GOKRAZY_FIRST_START") == "1" {
156 | // Do not supervise this process: it is meant for interactive usage on a
157 | // terminal. If you are looking for a daemon, use gokr-webstat instead.
158 | os.Exit(125)
159 | }
160 |
161 | if err := printStats(); err != nil {
162 | log.Fatal(err)
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/cmd/gokr-webstat/webstat.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "flag"
7 | "fmt"
8 | "html/template"
9 | "io"
10 | "io/ioutil"
11 | "log"
12 | "net/http"
13 | "os"
14 | "strings"
15 | "sync"
16 | "time"
17 |
18 | "github.com/gokrazy/stat"
19 | "github.com/gokrazy/stat/internal/statflag"
20 | "golang.org/x/sync/errgroup"
21 | )
22 |
23 | func formatCols(cols []stat.Col) string {
24 | formatted := make([]string, len(cols))
25 | for idx, col := range cols {
26 | formatted[idx] = fmt.Sprintf(
27 | `%s | `,
28 | col.Width,
29 | col.HTML())
30 | }
31 | return strings.Join(formatted, "\n")
32 | }
33 |
34 | func serveStats() error {
35 | var listen = flag.String("listen", ":6618", "[host]:port to serve HTML on")
36 | var enabledModules = flag.String("modules", "cpu,disk,sys,net,mem", "comma-separated list of modules to show. known modules: cpu,disk,sys,net,mem,thermal")
37 | flag.Parse()
38 |
39 | modules, err := statflag.ModulesFromFlag(*enabledModules)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | var headers []string
45 | for _, mod := range modules {
46 | hdrs := mod.Headers()
47 | for idx, h := range hdrs {
48 | hdrs[idx] = strings.ReplaceAll(h, " ", "_")
49 | }
50 | headers = append(headers, hdrs...)
51 | }
52 |
53 | parts := make([]string, len(modules))
54 | files := make(map[string]*os.File)
55 | for _, mod := range modules {
56 | // When a stats module implements the FileContents() interface, we
57 | // ensure all returned file contents are read and passed to
58 | // ProcessAndFormat.
59 | fc, ok := mod.(interface{ FileContents() []string })
60 | if !ok {
61 | continue
62 | }
63 | for _, f := range fc.FileContents() {
64 | if _, ok := files[f]; ok {
65 | continue // already requested
66 | }
67 | fl, err := os.Open(f)
68 | if err != nil {
69 | return err
70 | }
71 | files[f] = fl
72 | }
73 | }
74 |
75 | statusTmpl, err := template.New("").Parse(statusTmpl)
76 | if err != nil {
77 | return err
78 | }
79 | hostname, _ := os.Hostname()
80 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
81 | if r.URL.Path != "/" {
82 | http.Error(w, "not found", http.StatusNotFound)
83 | return
84 | }
85 | var buf bytes.Buffer
86 | if err := statusTmpl.Execute(&buf, struct {
87 | Hostname string
88 | Headers []string
89 | }{
90 | Hostname: hostname,
91 | Headers: headers,
92 | }); err != nil {
93 | log.Print(err)
94 | http.Error(w, err.Error(), http.StatusInternalServerError)
95 | return
96 | }
97 | io.Copy(w, &buf)
98 | })
99 | var newEventMu sync.Mutex
100 | newEvent := sync.NewCond(&newEventMu)
101 | http.HandleFunc("/readings", func(w http.ResponseWriter, r *http.Request) {
102 | w.Header().Set("Content-Type", "text/event-stream")
103 | newEventMu.Lock()
104 | for {
105 | b, err := json.Marshal(parts)
106 | if err != nil {
107 | log.Print(err)
108 | http.Error(w, err.Error(), http.StatusInternalServerError)
109 | return
110 | }
111 | rdr := bytes.NewReader(append(append([]byte("data: "), b...), []byte("\n\n")...))
112 | newEventMu.Unlock()
113 |
114 | if _, err := io.Copy(w, rdr); err != nil {
115 | log.Print(err)
116 | return
117 | }
118 | if f, ok := w.(http.Flusher); ok {
119 | f.Flush()
120 | }
121 |
122 | newEventMu.Lock()
123 | newEvent.Wait()
124 | }
125 |
126 | })
127 | var eg errgroup.Group
128 | eg.Go(func() error {
129 | for i := 0; ; i++ {
130 | contents := make(map[string][]byte)
131 | for path, fl := range files {
132 | if _, err := fl.Seek(0, io.SeekStart); err != nil {
133 | return err
134 | }
135 | b, err := ioutil.ReadAll(fl)
136 | if err != nil {
137 | return err
138 | }
139 | contents[path] = b
140 | }
141 |
142 | newEventMu.Lock()
143 | for idx, mod := range modules {
144 | parts[idx] = formatCols(mod.ProcessAndFormat(contents))
145 | }
146 | newEventMu.Unlock()
147 |
148 | if i > 0 {
149 | const darkblue = "\033[0;34m"
150 | //fmt.Println(strings.Join(parts, darkblue+" | "))
151 | // TODO: clear colors at the end of line so that program can be interrupted
152 | newEvent.Broadcast()
153 | }
154 |
155 | time.Sleep(1 * time.Second)
156 | }
157 | })
158 | eg.Go(func() error {
159 | log.Printf("listening on %s", *listen)
160 | return http.ListenAndServe(*listen, nil)
161 | })
162 | return eg.Wait()
163 | }
164 |
165 | func main() {
166 | if err := serveStats(); err != nil {
167 | log.Fatal(err)
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/stat.go:
--------------------------------------------------------------------------------
1 | package stat
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | const (
10 | UnitNone = iota
11 | UnitBytes
12 | UnitBytesFloat
13 | UnitMetric
14 | )
15 |
16 | var colorNameToANSI = map[string]string{
17 | "darkgray": "\033[1;30m",
18 | "red": "\033[1;31m",
19 | "green": "\033[1;32m",
20 | "yellow": "\033[1;33m",
21 | "blue": "\033[1;34m",
22 | "magenta": "\033[1;35m",
23 | "cyan": "\033[1;36m",
24 | "white": "\033[1;37m",
25 | }
26 |
27 | var colorNameToHTML = map[string]string{
28 | "darkgray": "#555753",
29 | "red": "#EF2929",
30 | "green": "#8AE234",
31 | "yellow": "#FCE94F",
32 | "blue": "#729FCF",
33 | "magenta": "#EE38DA",
34 | "cyan": "#34E2E2",
35 | "white": "#EEEEEC",
36 | }
37 |
38 | const (
39 | black = "\033[0;30m"
40 | darkred = "\033[0;31m"
41 | darkgreen = "\033[0;32m"
42 | darkyellow = "\033[0;33m"
43 | darkblue = "\033[0;34m"
44 | darkmagenta = "\033[0;35m"
45 | darkcyan = "\033[0;36m"
46 | gray = "\033[0;37m"
47 |
48 | darkgray = "darkgray"
49 | red = "red"
50 | green = "green"
51 | yellow = "yellow"
52 | blue = "blue"
53 | magenta = "magenta"
54 | cyan = "cyan"
55 | white = "white"
56 |
57 | blackbg = "\033[40m"
58 | redbg = "\033[41m"
59 | greenbg = "\033[42m"
60 | yellowbg = "\033[43m"
61 | bluebg = "\033[44m"
62 | magentabg = "\033[45m"
63 | cyanbg = "\033[46m"
64 | whitebg = "\033[47m"
65 | )
66 |
67 | var colors = []string{
68 | red,
69 | yellow,
70 | green,
71 | blue,
72 | cyan,
73 | white,
74 | darkred,
75 | darkgreen,
76 | }
77 |
78 | type ColType int
79 |
80 | const (
81 | ColGauge ColType = iota
82 | ColPercentage
83 | )
84 |
85 | type Col struct {
86 | Type ColType
87 | ValU64 uint64
88 | ValInt int // used by ColPercentage
89 | ValFloat64 float64 // used by UnitBytes
90 | Unit int
91 | Width int
92 | Scale int
93 | }
94 |
95 | func (c Col) WithWidth(w int) Col {
96 | ret := c
97 | ret.Width = w
98 | return ret
99 | }
100 |
101 | var unitsuffix = []string{"B", "k", "M", "G", "T"}
102 |
103 | func (c Col) String() string {
104 | return c.colorize(func(col, text string) string {
105 | return colorNameToANSI[col] + text
106 | })
107 | }
108 |
109 | func (c Col) HTML() string {
110 | return c.colorize(func(col, text string) string {
111 | return fmt.Sprintf(`%s`, colorNameToHTML[col], text)
112 | })
113 | }
114 |
115 | func (c Col) RenderCustom(colorFunc func(col, text string) string) string {
116 | return c.colorize(func(col, text string) string {
117 | return colorFunc(col, text)
118 | })
119 | }
120 |
121 | func (c Col) colorize(color func(col, text string) string) string {
122 | switch c.Type {
123 | case ColPercentage:
124 | v := fmt.Sprintf("%"+strconv.Itoa(c.Width)+"d", int(c.ValFloat64))
125 | col := colors[int(c.ValFloat64/float64(c.Scale))%len(colors)]
126 | if strings.TrimSpace(v) == "0" {
127 | return color(darkgray, v)
128 | }
129 | if c.ValFloat64 >= 100 {
130 | return color(white, v)
131 | }
132 | return color(col, v)
133 |
134 | case ColGauge:
135 | if c.Unit == UnitBytes || c.Unit == UnitBytesFloat ||
136 | c.Unit == UnitMetric {
137 | base := 1024
138 | if c.Unit == UnitMetric {
139 | base = 1000
140 | }
141 | width := c.Width
142 | if c.ValU64 == 0 && c.ValFloat64 == 0 {
143 | return color(darkgray, fmt.Sprintf("%"+strconv.Itoa(width)+"d", 0))
144 | }
145 | width-- // for the unit suffix
146 | var f string
147 | var cl int
148 | if c.Unit == UnitBytesFloat {
149 | f, cl = fchg(c.ValFloat64, width, base)
150 | } else {
151 | f, cl = dchg(c.ValU64, width, base)
152 | }
153 | if len(f) < width {
154 | f = strings.Repeat(" ", width-len(f)) + f
155 | }
156 | if cl > 0 || c.Unit == UnitBytes || c.Unit == UnitBytesFloat {
157 | if cl < len(unitsuffix) {
158 | f += color(darkgray, unitsuffix[cl])
159 | } else {
160 | f += "?"
161 | }
162 | } else {
163 | f += " " // empty suffix
164 | }
165 | col := colors[cl%len(colors)]
166 | return color(col, f)
167 | }
168 | return fmt.Sprintf("%4d", c.ValU64)
169 |
170 | default:
171 | return "?BUG?"
172 | }
173 | }
174 |
175 | func ByteCol(v uint64) Col {
176 | return Col{
177 | Type: ColGauge,
178 | Unit: UnitBytes,
179 | ValU64: v,
180 | }
181 | }
182 |
183 | func MetricCol(v uint64) Col {
184 | return Col{
185 | Type: ColGauge,
186 | Unit: UnitMetric,
187 | ValU64: v,
188 | }
189 | }
190 |
191 | // -----------------------------------------------------------------------------
192 | // dchg and fchg were ported directly from dstat.
193 | // -----------------------------------------------------------------------------
194 |
195 | func dchg(v uint64, width int, base int) (string, int) {
196 | c := 0
197 | for {
198 | ret := strconv.FormatUint(v, 10)
199 | if len(ret) <= width {
200 | return ret, c
201 | }
202 | v = v / uint64(base)
203 | c++
204 | }
205 | }
206 |
207 | func fchg(v float64, width int, base int) (string, int) {
208 | if v == 0 {
209 | return "0", 0
210 | }
211 | c := 0
212 | var ret string
213 | for {
214 | ret = strconv.Itoa(int(v))
215 | if len(ret) <= width {
216 | i := width - len(ret) - 1
217 | for ; i > 0; i-- {
218 | ret = strconv.FormatFloat(v, 'f', i, 64)
219 | if len(ret) <= width {
220 | break
221 | }
222 | }
223 | if i == 0 {
224 | ret = strconv.Itoa(int(v))
225 | }
226 | break
227 | }
228 | v = v / float64(base)
229 | c++
230 | }
231 | return ret, c
232 | }
233 |
--------------------------------------------------------------------------------