Skip to content

Commit 8a2b841

Browse files
committed
feat: add coder stat command for simpler agent metadata declarations
1 parent 202b6fc commit 8a2b841

10 files changed

+251
-29
lines changed

cli/root.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,15 @@ func (r *RootCmd) checkVersions(i *clibase.Invocation, client *codersdk.Client)
680680
return nil
681681
}
682682

683+
// verboseStderr returns the stderr writer if verbose is set, otherwise
684+
// it returns a discard writer.
685+
func (r *RootCmd) verboseStderr(inv *clibase.Invocation) io.Writer {
686+
if r.verbose {
687+
return inv.Stderr
688+
}
689+
return io.Discard
690+
}
691+
683692
func (r *RootCmd) checkWarnings(i *clibase.Invocation, client *codersdk.Client) error {
684693
if r.noFeatureWarning {
685694
return nil

cli/stat.go

Lines changed: 117 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,167 @@
11
package cli
22

33
import (
4+
"bufio"
5+
"bytes"
46
"fmt"
7+
"os"
8+
"strconv"
9+
"strings"
510
"time"
611

712
"github.com/shirou/gopsutil/cpu"
13+
"golang.org/x/xerrors"
814

915
"github.com/coder/coder/cli/clibase"
16+
"github.com/coder/coder/cli/cliui"
1017
)
1118

1219
type statCmd struct {
20+
*RootCmd
1321
watch time.Duration
1422
}
1523

16-
func (*RootCmd) stat() *clibase.Cmd {
24+
func (r *RootCmd) stat() *clibase.Cmd {
1725
c := &clibase.Cmd{
18-
Use: "stat <type> [flags...]",
19-
Short: "Display system resource usage statistics",
20-
Long: "stat can be used as the script for agent metadata blocks.",
21-
Hidden: true,
26+
Use: "stat <type> [flags...]",
27+
Short: "Display local system resource usage statistics",
28+
Long: "stat calls can be used as the script for agent metadata blocks.",
2229
}
23-
var statCmd statCmd
30+
sc := statCmd{RootCmd: r}
2431
c.Options.Add(
2532
clibase.Option{
2633
Flag: "watch",
2734
FlagShorthand: "w",
2835
Description: "Continuously display the statistic on the given interval.",
29-
Value: clibase.DurationOf(&statCmd.watch),
36+
Value: clibase.DurationOf(&sc.watch),
3037
},
3138
)
3239
c.AddSubcommands(
33-
statCmd.cpu(),
40+
sc.cpu(),
3441
)
42+
sc.setWatchLoops(c)
3543
return c
3644
}
3745

38-
func (sc *statCmd) watchLoop(fn clibase.HandlerFunc) clibase.HandlerFunc {
39-
return func(inv *clibase.Invocation) error {
40-
if sc.watch == 0 {
41-
return fn(inv)
42-
}
46+
func (sc *statCmd) setWatchLoops(c *clibase.Cmd) {
47+
for _, cmd := range c.Children {
48+
innerHandler := cmd.Handler
49+
cmd.Handler = func(inv *clibase.Invocation) error {
50+
if sc.watch == 0 {
51+
return innerHandler(inv)
52+
}
4353

44-
ticker := time.NewTicker(sc.watch)
45-
defer ticker.Stop()
54+
ticker := time.NewTicker(sc.watch)
55+
defer ticker.Stop()
4656

47-
for range ticker.C {
48-
if err := fn(inv); err != nil {
49-
_, _ = fmt.Fprintf(inv.Stderr, "error: %v", err)
57+
for range ticker.C {
58+
if err := innerHandler(inv); err != nil {
59+
_, _ = fmt.Fprintf(inv.Stderr, "error: %v", err)
60+
}
5061
}
62+
panic("unreachable")
63+
}
64+
}
65+
}
66+
67+
func cpuUsageFromCgroup(interval time.Duration) (float64, error) {
68+
cgroup, err := os.OpenFile("/proc/self/cgroup", os.O_RDONLY, 0)
69+
if err != nil {
70+
return 0, err
71+
}
72+
defer cgroup.Close()
73+
sc := bufio.NewScanner(cgroup)
74+
75+
var groupDir string
76+
for sc.Scan() {
77+
fields := strings.Split(sc.Text(), ":")
78+
if len(fields) != 3 {
79+
continue
5180
}
52-
panic("unreachable")
81+
if fields[1] != "cpu,cpuacct" {
82+
continue
83+
}
84+
groupDir = fields[2]
85+
break
86+
}
87+
88+
if groupDir == "" {
89+
return 0, xerrors.New("no cpu cgroup found")
90+
}
91+
92+
cpuAcct := func() (int64, error) {
93+
path := fmt.Sprintf("/sys/fs/cgroup/cpu,cpuacct/%s/cpuacct.usage", groupDir)
94+
95+
byt, err := os.ReadFile(
96+
path,
97+
)
98+
if err != nil {
99+
return 0, err
100+
}
101+
102+
return strconv.ParseInt(string(bytes.TrimSpace(byt)), 10, 64)
53103
}
104+
105+
stat1, err := cpuAcct()
106+
if err != nil {
107+
return 0, err
108+
}
109+
110+
time.Sleep(interval)
111+
112+
stat2, err := cpuAcct()
113+
if err != nil {
114+
return 0, err
115+
}
116+
117+
var (
118+
cpuTime = time.Duration(stat2 - stat1)
119+
realTime = interval
120+
)
121+
122+
ncpu, err := cpu.Counts(true)
123+
if err != nil {
124+
return 0, err
125+
}
126+
127+
return (cpuTime.Seconds() / realTime.Seconds()) * 100 / float64(ncpu), nil
54128
}
55129

56130
//nolint:revive
57131
func (sc *statCmd) cpu() *clibase.Cmd {
58132
var interval time.Duration
59133
c := &clibase.Cmd{
60-
Use: "cpu",
61-
Short: "Display the system's cpu usage",
62-
Long: "Display the system's load average.",
63-
Handler: sc.watchLoop(func(inv *clibase.Invocation) error {
64-
r, err := cpu.Percent(0, false)
134+
Use: "cpu-usage",
135+
Aliases: []string{"cu"},
136+
Short: "Display the system's cpu usage",
137+
Long: "If inside a cgroup (e.g. docker container), the cpu usage is ",
138+
Handler: func(inv *clibase.Invocation) error {
139+
if sc.watch != 0 {
140+
interval = sc.watch
141+
}
142+
143+
r, err := cpuUsageFromCgroup(interval)
65144
if err != nil {
66-
return err
145+
cliui.Infof(sc.verboseStderr(inv), "cgroup error: %+v", err)
146+
147+
// Use standard methods if cgroup method fails.
148+
rs, err := cpu.Percent(interval, false)
149+
if err != nil {
150+
return err
151+
}
152+
r = rs[0]
67153
}
68-
_, _ = fmt.Fprintf(inv.Stdout, "%02.0f\n", r[0])
154+
155+
_, _ = fmt.Fprintf(inv.Stdout, "%02.0f\n", r)
156+
69157
return nil
70-
}),
158+
},
71159
Options: []clibase.Option{
72160
{
73161
Flag: "interval",
74162
FlagShorthand: "i",
75-
Description: "The sample collection interval.",
76-
Default: "1s",
163+
Description: `The sample collection interval. If --watch is set, it overrides this value.`,
164+
Default: "0s",
77165
Value: clibase.DurationOf(&interval),
78166
},
79167
},

cli/stat_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/cli/clitest"
10+
)
11+
12+
func TestStat(t *testing.T) {
13+
t.Parallel()
14+
15+
t.Run("cpu", func(t *testing.T) {
16+
t.Parallel()
17+
inv, _ := clitest.New(t, "stat", "cpu")
18+
var out bytes.Buffer
19+
inv.Stdout = &out
20+
clitest.Run(t, inv)
21+
22+
require.Regexp(t, `^[\d]{2}\n$`, out.String())
23+
})
24+
}

cli/testdata/coder_--help.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Coder v0.0.0-devel — A tool for provisioning self-hosted development environme
3434
workspace
3535
ssh Start a shell into a workspace
3636
start Start a workspace
37+
stat Display local system resource usage statistics
3738
state Manually manage Terraform state to fix broken workspaces
3839
stop Stop a workspace
3940
templates Manage templates

cli/testdata/coder_stat_--help.golden

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Usage: coder stat [flags] <type> [flags...]
2+
3+
Display local system resource usage statistics
4+
5+
stat calls can be used as the script for agent metadata blocks.
6+
7+
Subcommands
8+
cpu Display the system's cpu usage
9+
10+
Options
11+
-w, --watch duration
12+
Continuously display the statistic on the given interval.
13+
14+
---
15+
Run `coder --help` for a list of global options.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Usage: coder stat cpu [flags]
2+
3+
Display the system's cpu usage
4+
5+
If inside a cgroup (e.g. docker container), the cpu usage is
6+
7+
Options
8+
-i, --interval duration (default: 0s)
9+
The sample collection interval. If --watch is set, it overrides this
10+
value.
11+
12+
---
13+
Run `coder --help` for a list of global options.

docs/cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
4949
| [<code>speedtest</code>](./cli/speedtest) | Run upload and download tests from your machine to a workspace |
5050
| [<code>ssh</code>](./cli/ssh) | Start a shell into a workspace |
5151
| [<code>start</code>](./cli/start) | Start a workspace |
52+
| [<code>stat</code>](./cli/stat) | Display local system resource usage statistics |
5253
| [<code>state</code>](./cli/state) | Manually manage Terraform state to fix broken workspaces |
5354
| [<code>stop</code>](./cli/stop) | Stop a workspace |
5455
| [<code>templates</code>](./cli/templates) | Manage templates |

docs/cli/stat.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!-- DO NOT EDIT | GENERATED CONTENT -->
2+
3+
# stat
4+
5+
Display local system resource usage statistics
6+
7+
## Usage
8+
9+
```console
10+
coder stat [flags] <type> [flags...]
11+
```
12+
13+
## Description
14+
15+
```console
16+
stat calls can be used as the script for agent metadata blocks.
17+
```
18+
19+
## Subcommands
20+
21+
| Name | Purpose |
22+
| ------------------------------ | ------------------------------ |
23+
| [<code>cpu</code>](./stat_cpu) | Display the system's cpu usage |
24+
25+
## Options
26+
27+
### -w, --watch
28+
29+
| | |
30+
| ---- | --------------------- |
31+
| Type | <code>duration</code> |
32+
33+
Continuously display the statistic on the given interval.

docs/cli/stat_cpu.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!-- DO NOT EDIT | GENERATED CONTENT -->
2+
3+
# stat cpu
4+
5+
Display the system's cpu usage
6+
7+
## Usage
8+
9+
```console
10+
coder stat cpu [flags]
11+
```
12+
13+
## Description
14+
15+
```console
16+
If inside a cgroup (e.g. docker container), the cpu usage is
17+
```
18+
19+
## Options
20+
21+
### -i, --interval
22+
23+
| | |
24+
| ------- | --------------------- |
25+
| Type | <code>duration</code> |
26+
| Default | <code>0s</code> |
27+
28+
The sample collection interval. If --watch is set, it overrides this value.

docs/manifest.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,16 @@
677677
"description": "Start a workspace",
678678
"path": "cli/start.md"
679679
},
680+
{
681+
"title": "stat",
682+
"description": "Display local system resource usage statistics",
683+
"path": "cli/stat.md"
684+
},
685+
{
686+
"title": "stat cpu",
687+
"description": "Display the system's cpu usage",
688+
"path": "cli/stat_cpu.md"
689+
},
680690
{
681691
"title": "state",
682692
"description": "Manually manage Terraform state to fix broken workspaces",

0 commit comments

Comments
 (0)