coder/cli/stat.go

293 lines
6.8 KiB
Go

package cli
import (
"fmt"
"os"
"github.com/spf13/afero"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clistat"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/serpent"
)
func initStatterMW(tgt **clistat.Statter, fs afero.Fs) serpent.MiddlewareFunc {
return func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(i *serpent.Invocation) error {
var err error
stat, err := clistat.New(clistat.WithFS(fs))
if err != nil {
return xerrors.Errorf("initialize workspace stats collector: %w", err)
}
*tgt = stat
return next(i)
}
}
}
func (r *RootCmd) stat() *serpent.Command {
var (
st *clistat.Statter
fs = afero.NewReadOnlyFs(afero.NewOsFs())
formatter = cliui.NewOutputFormatter(
cliui.TableFormat([]statsRow{}, []string{
"host_cpu",
"host_memory",
"home_disk",
"container_cpu",
"container_memory",
}),
cliui.JSONFormat(),
)
)
cmd := &serpent.Command{
Use: "stat",
Short: "Show resource usage for the current workspace.",
Middleware: initStatterMW(&st, fs),
Children: []*serpent.Command{
r.statCPU(fs),
r.statMem(fs),
r.statDisk(fs),
},
Handler: func(inv *serpent.Invocation) error {
var sr statsRow
// Get CPU measurements first.
hostErr := make(chan error, 1)
containerErr := make(chan error, 1)
go func() {
defer close(hostErr)
cs, err := st.HostCPU()
if err != nil {
hostErr <- err
return
}
sr.HostCPU = cs
}()
go func() {
defer close(containerErr)
if ok, _ := clistat.IsContainerized(fs); !ok {
// don't error if we're not in a container
return
}
cs, err := st.ContainerCPU()
if err != nil {
containerErr <- err
return
}
sr.ContainerCPU = cs
}()
if err := <-hostErr; err != nil {
return err
}
if err := <-containerErr; err != nil {
return err
}
// Host-level stats
ms, err := st.HostMemory(clistat.PrefixGibi)
if err != nil {
return err
}
sr.HostMemory = ms
home, err := os.UserHomeDir()
if err != nil {
return err
}
ds, err := st.Disk(clistat.PrefixGibi, home)
if err != nil {
return err
}
sr.Disk = ds
// Container-only stats.
if ok, err := clistat.IsContainerized(fs); err == nil && ok {
cs, err := st.ContainerCPU()
if err != nil {
return err
}
sr.ContainerCPU = cs
ms, err := st.ContainerMemory(clistat.PrefixGibi)
if err != nil {
return err
}
sr.ContainerMemory = ms
}
out, err := formatter.Format(inv.Context(), []statsRow{sr})
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
func (*RootCmd) statCPU(fs afero.Fs) *serpent.Command {
var (
hostArg bool
st *clistat.Statter
formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
)
cmd := &serpent.Command{
Use: "cpu",
Short: "Show CPU usage, in cores.",
Middleware: initStatterMW(&st, fs),
Options: serpent.OptionSet{
{
Flag: "host",
Value: serpent.BoolOf(&hostArg),
Description: "Force host CPU measurement.",
},
},
Handler: func(inv *serpent.Invocation) error {
var cs *clistat.Result
var err error
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
cs, err = st.ContainerCPU()
} else {
cs, err = st.HostCPU()
}
if err != nil {
return err
}
out, err := formatter.Format(inv.Context(), cs)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
func (*RootCmd) statMem(fs afero.Fs) *serpent.Command {
var (
hostArg bool
prefixArg string
st *clistat.Statter
formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
)
cmd := &serpent.Command{
Use: "mem",
Short: "Show memory usage, in gigabytes.",
Middleware: initStatterMW(&st, fs),
Options: serpent.OptionSet{
{
Flag: "host",
Value: serpent.BoolOf(&hostArg),
Description: "Force host memory measurement.",
},
{
Description: "SI Prefix for memory measurement.",
Default: clistat.PrefixHumanGibi,
Flag: "prefix",
Value: serpent.EnumOf(&prefixArg,
clistat.PrefixHumanKibi,
clistat.PrefixHumanMebi,
clistat.PrefixHumanGibi,
clistat.PrefixHumanTebi,
),
},
},
Handler: func(inv *serpent.Invocation) error {
pfx := clistat.ParsePrefix(prefixArg)
var ms *clistat.Result
var err error
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
ms, err = st.ContainerMemory(pfx)
} else {
ms, err = st.HostMemory(pfx)
}
if err != nil {
return err
}
out, err := formatter.Format(inv.Context(), ms)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
func (*RootCmd) statDisk(fs afero.Fs) *serpent.Command {
var (
pathArg string
prefixArg string
st *clistat.Statter
formatter = cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
)
cmd := &serpent.Command{
Use: "disk",
Short: "Show disk usage, in gigabytes.",
Middleware: initStatterMW(&st, fs),
Options: serpent.OptionSet{
{
Flag: "path",
Value: serpent.StringOf(&pathArg),
Description: "Path for which to check disk usage.",
Default: "/",
},
{
Flag: "prefix",
Default: clistat.PrefixHumanGibi,
Description: "SI Prefix for disk measurement.",
Value: serpent.EnumOf(&prefixArg,
clistat.PrefixHumanKibi,
clistat.PrefixHumanMebi,
clistat.PrefixHumanGibi,
clistat.PrefixHumanTebi,
),
},
},
Handler: func(inv *serpent.Invocation) error {
pfx := clistat.ParsePrefix(prefixArg)
// Users may also call `coder stat disk <path>`.
if len(inv.Args) > 0 {
pathArg = inv.Args[0]
}
ds, err := st.Disk(pfx, pathArg)
if err != nil {
if os.IsNotExist(err) {
//nolint:gocritic // fmt.Errorf produces a more concise error.
return fmt.Errorf("not found: %q", pathArg)
}
return err
}
out, err := formatter.Format(inv.Context(), ds)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}
type statsRow struct {
HostCPU *clistat.Result `json:"host_cpu" table:"host_cpu,default_sort"`
HostMemory *clistat.Result `json:"host_memory" table:"host_memory"`
Disk *clistat.Result `json:"home_disk" table:"home_disk"`
ContainerCPU *clistat.Result `json:"container_cpu" table:"container_cpu"`
ContainerMemory *clistat.Result `json:"container_memory" table:"container_memory"`
}