├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── topic │ └── topic.go ├── doc └── image │ └── topic.png ├── go.mod ├── go.sum └── pkg ├── cpu.go ├── load.go ├── memory.go ├── tasks.go ├── user.go └── util.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | cmd/topic/topic 3 | topic -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | COPY topic /bin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 silenceshell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | GOOS=linux go build cmd/topic/topic.go 3 | 4 | image: build 5 | docker build . -t silenceshell/topic 6 | 7 | clean: 8 | rm -f topic 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # topic 2 | 3 | top in container. 4 | 5 | Running the original `top` command in a container will not get information of the container, many metrics like uptime, users, load average, tasks, cpu, memory, are about the host in fact. 6 | `topic`(**top** **i**n **c**ontainer) will retrieve those metrics from container instead, and shows the status of the container, not the host. 7 | 8 | Below shows a container of 2 cpu and 2 Gi running status when stress with `--cpu 2`. 9 | 10 | ![topic.png](doc/image/topic.png) 11 | 12 | ## How to use 13 | 14 | Download `topic` from GitHub [release page](https://github.com/silenceshell/topic/releases) to the container which you want to inspect, and add `x` attribute to the binary, then run the binary! 15 | 16 | Or just create a container for a try: 17 | 18 | ```sh 19 | # start a 2c2g container 20 | $ docker run -it --name topic --rm --cpus 2 --memory 2g silenceshell/topic bash 21 | # run topic in this container 22 | root@04065eeff138:/# topic 23 | ``` 24 | 25 | 26 | ## How to build 27 | 28 | Run `make build` for linux and amd64. `topic` only support on linux. If you want to run on other architectures, `GOARCH` is need to be set. 29 | -------------------------------------------------------------------------------- /cmd/topic/topic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "time" 8 | 9 | linuxproc "github.com/c9s/goprocinfo/linux" 10 | ui "github.com/gizak/termui/v3" 11 | "github.com/gizak/termui/v3/widgets" 12 | 13 | "topic/pkg" 14 | ) 15 | 16 | const ( 17 | menuPrint = " PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND" 18 | ) 19 | 20 | var ( 21 | termWidth = 0 22 | termHeight = 0 23 | loadMonitor *pkg.LoadMonitor 24 | ) 25 | 26 | func genSummary(stat *linuxproc.Stat) []string { 27 | currentTime := time.Now().Local().Format("15:04:05") 28 | upTime := pkg.GetContainerUpTime(stat) 29 | tc := pkg.GetTaskCount() 30 | cpuCount := pkg.GetCpuCount(stat) 31 | userCpu, systemCpu, idleCpu := pkg.GetCpuUsage(cpuCount) 32 | memInfo := pkg.GetTotalMemInMiB() 33 | avail := memInfo.Free + memInfo.Cache 34 | return []string{ 35 | fmt.Sprintf("topic - %v up %s, %d users, load average: %s", currentTime, upTime, pkg.GetUsers(), loadMonitor.GetLoad()), 36 | fmt.Sprintf("Tasks: [%3d](mod:bold) total, [%3d](mod:bold) running, [%3d](mod:bold) sleeping, [%3d](mod:bold) stopped, [%3d](mod:bold) zombie", 37 | tc.Total, tc.Running, tc.Sleeping, tc.Stopped, tc.Zombie), 38 | fmt.Sprintf("%%Cpu(%sc): [%2.1f](mod:bold) us, [%2.1f](mod:bold) sy, [0.0](mod:bold) ni, [%2.1f](mod:bold) id, [0.0](mod:bold) wa, [0.0 hi,](mod:bold) [0.0](mod:bold) si, [0.0](mod:bold) st", 39 | pkg.CpuCountToString(cpuCount), userCpu, systemCpu, idleCpu), 40 | fmt.Sprintf("MiB Mem : [%7.1f](mod:bold) total, [%7.1f](mod:bold) free, [%7.1f](mod:bold) used, [%7.1f](mod:bold) buff/cache", 41 | memInfo.Total, memInfo.Free, memInfo.Used, memInfo.Cache), 42 | fmt.Sprintf("MiB Swap: [0](mod:bold) total, [0](mod:bold) free, [0](mod:bold) used. [%7.1f](mod:bold) avail Mem", avail), 43 | } 44 | } 45 | 46 | func genMenu() string { 47 | spaceFmt := fmt.Sprintf("%%%vs", termWidth-len(menuPrint)-1) 48 | paddingSpace := fmt.Sprintf(spaceFmt, " ") 49 | return fmt.Sprintf("[%s%s](fg:black,bg:white)", menuPrint, paddingSpace) 50 | } 51 | 52 | func genProcesses(taskMonitor *pkg.TaskMonitor) string { 53 | result := []string{ 54 | genMenu(), 55 | } 56 | result = append(result, taskMonitor.GetTaskInfos()...) 57 | return strings.Join(result, "\n") 58 | } 59 | 60 | func main() { 61 | if err := ui.Init(); err != nil { 62 | log.Fatalf("failed to initialize termui: %v", err) 63 | } 64 | defer ui.Close() 65 | 66 | stat, err := linuxproc.ReadStat("/proc/stat") 67 | if err != nil { 68 | log.Fatal("stat read fail") 69 | } 70 | 71 | termWidth, termHeight = ui.TerminalDimensions() 72 | 73 | taskMonitor := pkg.NewTaskMonitor(stat) 74 | 75 | loadMonitor = pkg.NewLoadMonitor() 76 | go loadMonitor.Run() 77 | 78 | summary := widgets.NewList() 79 | summary.Rows = genSummary(stat) 80 | summary.TextStyle = ui.NewStyle(ui.ColorWhite) 81 | summary.WrapText = false 82 | summary.SetRect(-1, -1, termWidth, termHeight) 83 | summary.Border = false 84 | 85 | processes := widgets.NewParagraph() 86 | processes.Text = genProcesses(taskMonitor) 87 | processes.WrapText = false 88 | processes.SetRect(-1, 5, termWidth, termHeight) 89 | processes.Border = false 90 | 91 | draw := func() { 92 | stat, err := linuxproc.ReadStat("/proc/stat") 93 | if err != nil { 94 | log.Fatal("stat read fail") 95 | } 96 | 97 | termWidth, termHeight = ui.TerminalDimensions() 98 | summary.Rows = genSummary(stat) 99 | processes.Text = genProcesses(taskMonitor) 100 | ui.Render(summary, processes) 101 | } 102 | draw() 103 | 104 | uiEvents := ui.PollEvents() 105 | ticker := time.NewTicker(time.Second).C 106 | for { 107 | select { 108 | case e := <-uiEvents: 109 | switch e.ID { 110 | case "q", "": 111 | return 112 | } 113 | case <-ticker: 114 | draw() 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /doc/image/topic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silenceshell/topic/668f651312216fdc524f93e76f4db4f9e60b02ea/doc/image/topic.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module topic 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 7 | github.com/containerd/cgroups v1.0.1 8 | github.com/gizak/termui/v3 v3.1.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK84APFuMvxqsk3tEIaKH/z4Rpu3g= 3 | github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= 4 | github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= 5 | github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= 6 | github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= 7 | github.com/coreos/go-systemd/v22 v22.1.0 h1:kq/SbG2BCKLkDKkjQf5OWwKWUKj1lgs3lFI4PxnR5lg= 8 | github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 14 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 15 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 16 | github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= 17 | github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= 18 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 19 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 20 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 21 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 22 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 23 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 24 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 25 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 28 | github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= 29 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 30 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 31 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 32 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= 33 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= 34 | github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= 35 | github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 36 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 37 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 40 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 41 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 42 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 43 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 44 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 45 | github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 46 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 47 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 48 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 49 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 50 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 51 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 52 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 53 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 54 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 55 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 56 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 57 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 61 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= 65 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 70 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 71 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 72 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 77 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 78 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | -------------------------------------------------------------------------------- /pkg/cpu.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | linuxproc "github.com/c9s/goprocinfo/linux" 6 | ) 7 | 8 | var ( 9 | prevUsageUser int64 = 0 10 | prevUsageSystem int64 = 0 11 | ) 12 | 13 | func GetCpuCount(stat *linuxproc.Stat) (count float64) { 14 | cfsQuota := getCgroupValueByPath("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") 15 | cfsPeriod := getCgroupValueByPath("/sys/fs/cgroup/cpu/cpu.cfs_period_us") 16 | 17 | if cfsQuota == -1 { 18 | return float64(len(stat.CPUStats)) 19 | } 20 | 21 | return float64(cfsQuota) / float64(cfsPeriod) 22 | } 23 | 24 | func CpuCountToString(c float64) string { 25 | if c == float64(int64(c)) { 26 | return fmt.Sprintf("%v", c) 27 | } 28 | return fmt.Sprintf("%0.1f", c) 29 | } 30 | 31 | // GetCpuUsage should be called every 1 seconds. not quite precise. 32 | func GetCpuUsage(cpus float64) (user, system, idle float64) { 33 | var currentUsageUser, currentUsageSystem int64 34 | currentUsageUser = getCgroupValueByPath("/sys/fs/cgroup/cpuacct/cpuacct.usage_user") 35 | currentUsageSystem = getCgroupValueByPath("/sys/fs/cgroup/cpuacct/cpuacct.usage_sys") 36 | 37 | if prevUsageUser == 0 && prevUsageSystem == 0 { 38 | prevUsageUser = currentUsageUser 39 | prevUsageSystem = currentUsageSystem 40 | return 41 | } 42 | 43 | user = float64(currentUsageUser-prevUsageUser) / 10000000 / cpus // / 1000,000,000 * 100 = /10,000,000 44 | system = float64(currentUsageSystem-prevUsageSystem) / 10000000 / cpus // / 1000,000,000 * 100 = /10,000,000 45 | idle = 100 - user - system 46 | if idle < 0 { 47 | idle = 0 48 | } 49 | 50 | prevUsageUser = currentUsageUser 51 | prevUsageSystem = currentUsageSystem 52 | 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /pkg/load.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const ( 9 | FSHIFT = 11 10 | FIXED_1 = (1 << FSHIFT) 11 | EXP_1 = 1884 /* 1/exp(5sec/1min) as fixed-point */ 12 | EXP_5 = 2014 /* 1/exp(5sec/5min) */ 13 | EXP_15 = 2037 /* 1/exp(5sec/15min) */ 14 | ) 15 | 16 | type LoadMonitor struct { 17 | AvenRun1 uint64 18 | AvenRun5 uint64 19 | AvenRun15 uint64 20 | } 21 | 22 | func (l *LoadMonitor) Run() { 23 | ticker := time.NewTicker(100 * time.Millisecond) 24 | for { 25 | select { 26 | case <-ticker.C: 27 | l.refreshLoad() 28 | } 29 | } 30 | } 31 | 32 | // load1 = load0 * exp + active * (1 - exp) 33 | func calcLoad(load0 uint64, exp uint64, active uint64) uint64 { 34 | if active > 0 { 35 | active = active * FIXED_1 36 | } 37 | load1 := load0*exp + active*(FIXED_1-exp) 38 | if active >= load0 { 39 | load1 += FIXED_1 - 1 40 | } 41 | 42 | return load1 / FIXED_1 43 | } 44 | 45 | func (l *LoadMonitor) refreshLoad() { 46 | tc := GetTaskCount() 47 | runPid := tc.Running + tc.Uninterruptible 48 | l.AvenRun1 = calcLoad(l.AvenRun1, EXP_1, uint64(runPid)) 49 | l.AvenRun5 = calcLoad(l.AvenRun5, EXP_5, uint64(runPid)) 50 | l.AvenRun15 = calcLoad(l.AvenRun15, EXP_15, uint64(runPid)) 51 | } 52 | 53 | func loadInt(x uint64) uint64 { 54 | return x >> FSHIFT 55 | } 56 | 57 | func loadFrac(x uint64) uint64 { 58 | return loadInt(((x) & (FIXED_1 - 1)) * 100) 59 | } 60 | 61 | func (l *LoadMonitor) GetLoad() string { 62 | a := l.AvenRun1 + (FIXED_1 / 200) 63 | b := l.AvenRun5 + (FIXED_1 / 200) 64 | c := l.AvenRun15 + (FIXED_1 / 200) 65 | return fmt.Sprintf("%d.%02d, %d.%02d, %d.%02d", loadInt(a), loadFrac(a), loadInt(b), loadFrac(b), loadInt(c), loadFrac(c)) 66 | } 67 | 68 | func NewLoadMonitor() *LoadMonitor { 69 | l := LoadMonitor{} 70 | return &l 71 | } 72 | -------------------------------------------------------------------------------- /pkg/memory.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | linuxproc "github.com/c9s/goprocinfo/linux" 5 | "github.com/containerd/cgroups" 6 | v1 "github.com/containerd/cgroups/stats/v1" 7 | ) 8 | 9 | type MemInfo struct { 10 | Total, Free, Used, Cache float64 11 | } 12 | 13 | func GetMemInfoInByte() (memInfo MemInfo) { 14 | stats := v1.Metrics{} 15 | mem := cgroups.NewMemory("/sys/fs/cgroup/", cgroups.IgnoreModules("memsw")) 16 | err := mem.Stat("", &stats) 17 | if err != nil { 18 | return 19 | } 20 | 21 | hostMemInfo, err := linuxproc.ReadMemInfo("/proc/meminfo") 22 | if err != nil { 23 | return 24 | } 25 | 26 | // convert Byte to MiB 27 | var total float64 28 | if stats.Memory.Usage.Limit/1024 > hostMemInfo.MemTotal { 29 | total = float64(hostMemInfo.MemTotal) * 1024 30 | } else { 31 | total = float64(stats.Memory.Usage.Limit) 32 | } 33 | used := float64(stats.Memory.Usage.Usage) 34 | cache := float64(stats.Memory.Cache) 35 | free := total - used 36 | return MemInfo{Total: total, Free: free, Used: used, Cache: cache} 37 | } 38 | 39 | func GetTotalMemInMiB() (memInfo MemInfo) { 40 | memInfo = GetMemInfoInByte() 41 | memInfo.Total = memInfo.Total / 1048576 42 | memInfo.Free = memInfo.Free / 1048576 43 | memInfo.Used = memInfo.Used / 1048576 44 | memInfo.Cache = memInfo.Cache / 1048576 45 | return memInfo 46 | } 47 | func GetTotalMemInKiB() (memInfo MemInfo) { 48 | memInfo = GetMemInfoInByte() 49 | memInfo.Total = memInfo.Total / 1024 50 | memInfo.Free = memInfo.Free / 1024 51 | memInfo.Used = memInfo.Used / 1024 52 | memInfo.Cache = memInfo.Cache / 1024 53 | return memInfo 54 | } 55 | -------------------------------------------------------------------------------- /pkg/tasks.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | 9 | linuxproc "github.com/c9s/goprocinfo/linux" 10 | ) 11 | 12 | func GetContainerUpTime(stat *linuxproc.Stat) string { 13 | btime := stat.BootTime 14 | 15 | process1, _ := linuxproc.ReadProcessStat("/proc/1/stat") 16 | startTime := process1.Starttime 17 | 18 | uptime := btime.Add((time.Duration(startTime) * 10) * time.Millisecond) 19 | 20 | timeSince := time.Since(uptime) 21 | if timeSince > time.Hour*24 { 22 | days := timeSince / time.Hour / 24 23 | hours := timeSince / time.Hour % 24 24 | minutes := timeSince / time.Minute % 60 25 | return fmt.Sprintf("%d days, %d:%d", days, hours, minutes) 26 | } 27 | return uptime.Local().Format("15:04") 28 | } 29 | 30 | type TaskCount struct { 31 | Total int 32 | Running int 33 | Sleeping int 34 | Stopped int 35 | Zombie int 36 | Uninterruptible int 37 | } 38 | 39 | func GetTaskCount() (c TaskCount) { 40 | f, err := os.Open("/proc") 41 | if err != nil { 42 | fmt.Println(err) 43 | return 44 | } 45 | files, err := f.Readdir(0) 46 | if err != nil { 47 | fmt.Println(err) 48 | return 49 | } 50 | 51 | for _, v := range files { 52 | fileName := v.Name() 53 | if fileName[0] < '0' || fileName[0] > '9' { 54 | continue 55 | } 56 | pid, err := strconv.Atoi(v.Name()) 57 | if err == nil { 58 | p, err := linuxproc.ReadProcessStat(fmt.Sprintf("/proc/%d/stat", pid)) 59 | if err != nil { 60 | return 61 | } 62 | switch p.State { 63 | case "R": 64 | c.Running++ 65 | case "D": 66 | c.Uninterruptible++ 67 | case "t", "T": 68 | c.Stopped++ 69 | case "Z": 70 | c.Zombie++ 71 | default: 72 | c.Sleeping++ 73 | } 74 | c.Total++ 75 | } 76 | } 77 | 78 | return 79 | } 80 | 81 | const ( 82 | taskInfoFmt = "%5d %-8s%4d%4d%8d %6d %6d %s %5.1f %5.1f %8s %-s" 83 | ) 84 | 85 | func convertDuration(t time.Duration) string { 86 | minute := t / time.Minute 87 | t = t % time.Minute 88 | second := t / time.Second 89 | t = t % time.Second 90 | milliseconds := t / time.Millisecond / 10 91 | return fmt.Sprintf("%d:%02d.%02d", minute, second, milliseconds) 92 | } 93 | 94 | func (t *TaskMonitor) GetTaskInfos() (infos []string) { 95 | f, err := os.Open("/proc") 96 | if err != nil { 97 | fmt.Println(err) 98 | return 99 | } 100 | files, err := f.Readdir(0) 101 | if err != nil { 102 | fmt.Println(err) 103 | return 104 | } 105 | 106 | selfPid := os.Getpid() 107 | 108 | memInfo := GetTotalMemInKiB() 109 | total := memInfo.Total 110 | 111 | taskInfos := make([]string, 0) 112 | for _, v := range files { 113 | fileName := v.Name() 114 | if fileName[0] < '0' || fileName[0] > '9' { 115 | continue 116 | } 117 | pid, err := strconv.Atoi(v.Name()) 118 | if err == nil { 119 | procInfo, err := linuxproc.ReadProcess(uint64(pid), "/proc") 120 | if err != nil { 121 | fmt.Println(err) 122 | return 123 | } 124 | 125 | // in KiB 126 | virt := procInfo.Stat.Vsize / 1024 127 | res := procInfo.Statm.Resident * 4 128 | shr := procInfo.Statm.Share * 4 129 | 130 | var cpuUsage uint64 131 | if t.taskPrevUser[pid] != 0 || t.taskPrevSystem[pid] != 0 { 132 | cpuUsage = procInfo.Stat.Utime + procInfo.Stat.Stime - t.taskPrevUser[pid] - t.taskPrevSystem[pid] 133 | } 134 | t.taskPrevUser[pid] = procInfo.Stat.Utime 135 | t.taskPrevSystem[pid] = procInfo.Stat.Stime 136 | 137 | memUsage := float64(res) / total * 100 138 | uptime := (time.Duration(procInfo.Stat.Utime+procInfo.Stat.Stime) * 10) * time.Millisecond 139 | 140 | taskInfo := fmt.Sprintf(taskInfoFmt, pid, "root", procInfo.Stat.Priority, procInfo.Stat.Nice, 141 | virt, res, shr, procInfo.Stat.State, float64(cpuUsage), memUsage, 142 | convertDuration(uptime), procInfo.Cmdline) 143 | if selfPid == pid { 144 | taskInfo = fmt.Sprintf("[%v](mod:bold)", taskInfo) 145 | } 146 | taskInfos = append(taskInfos, taskInfo) 147 | } 148 | } 149 | return taskInfos 150 | } 151 | 152 | type TaskMonitor struct { 153 | stat *linuxproc.Stat 154 | taskPrevUser map[int]uint64 155 | taskPrevSystem map[int]uint64 156 | } 157 | 158 | func NewTaskMonitor(stat *linuxproc.Stat) *TaskMonitor { 159 | taskMonitor := TaskMonitor{ 160 | stat: stat, 161 | taskPrevUser: make(map[int]uint64), 162 | taskPrevSystem: make(map[int]uint64), 163 | } 164 | return &taskMonitor 165 | } 166 | -------------------------------------------------------------------------------- /pkg/user.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | func GetUsers() int { 10 | f, err := os.Open("/dev/pts") 11 | if err != nil { 12 | fmt.Println(err) 13 | return 0 14 | } 15 | files, err := f.Readdir(0) 16 | if err != nil { 17 | fmt.Println(err) 18 | return 0 19 | } 20 | 21 | c := 0 22 | for _, v := range files { 23 | _, err := strconv.Atoi(v.Name()) 24 | if err == nil { 25 | c++ 26 | } 27 | } 28 | return c 29 | } 30 | -------------------------------------------------------------------------------- /pkg/util.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func getCgroupValueByPath(path string) int64 { 9 | data, err := os.ReadFile(path) 10 | if err != nil { 11 | return 0 12 | } 13 | 14 | var value int64 15 | n, err := fmt.Sscanf(string(data), "%d", &value) 16 | if err != nil || n != 1 { 17 | return 0 18 | } 19 | return value 20 | } 21 | --------------------------------------------------------------------------------