mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(cli): add coder stat command (#8005)
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
package clistat
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Paths for CGroupV1.
|
||||
// Ref: https://www.kernel.org/doc/Documentation/cgroup-v1/cpuacct.txt
|
||||
const (
|
||||
// CPU usage of all tasks in cgroup in nanoseconds.
|
||||
cgroupV1CPUAcctUsage = "/sys/fs/cgroup/cpu/cpuacct.usage"
|
||||
// Alternate path
|
||||
cgroupV1CPUAcctUsageAlt = "/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage"
|
||||
// CFS quota and period for cgroup in MICROseconds
|
||||
cgroupV1CFSQuotaUs = "/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us"
|
||||
cgroupV1CFSPeriodUs = "/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us"
|
||||
// Maximum memory usable by cgroup in bytes
|
||||
cgroupV1MemoryMaxUsageBytes = "/sys/fs/cgroup/memory/memory.max_usage_in_bytes"
|
||||
// Current memory usage of cgroup in bytes
|
||||
cgroupV1MemoryUsageBytes = "/sys/fs/cgroup/memory/memory.usage_in_bytes"
|
||||
// Other memory stats - we are interested in total_inactive_file
|
||||
cgroupV1MemoryStat = "/sys/fs/cgroup/memory/memory.stat"
|
||||
)
|
||||
|
||||
// Paths for CGroupV2.
|
||||
// Ref: https://docs.kernel.org/admin-guide/cgroup-v2.html
|
||||
const (
|
||||
// Contains quota and period in microseconds separated by a space.
|
||||
cgroupV2CPUMax = "/sys/fs/cgroup/cpu.max"
|
||||
// Contains current CPU usage under usage_usec
|
||||
cgroupV2CPUStat = "/sys/fs/cgroup/cpu.stat"
|
||||
// Contains current cgroup memory usage in bytes.
|
||||
cgroupV2MemoryUsageBytes = "/sys/fs/cgroup/memory.current"
|
||||
// Contains max cgroup memory usage in bytes.
|
||||
cgroupV2MemoryMaxBytes = "/sys/fs/cgroup/memory.max"
|
||||
// Other memory stats - we are interested in total_inactive_file
|
||||
cgroupV2MemoryStat = "/sys/fs/cgroup/memory.stat"
|
||||
)
|
||||
|
||||
// ContainerCPU returns the CPU usage of the container cgroup.
|
||||
// This is calculated as difference of two samples of the
|
||||
// CPU usage of the container cgroup.
|
||||
// The total is read from the relevant path in /sys/fs/cgroup.
|
||||
// If there is no limit set, the total is assumed to be the
|
||||
// number of host cores multiplied by the CFS period.
|
||||
// If the system is not containerized, this always returns nil.
|
||||
func (s *Statter) ContainerCPU() (*Result, error) {
|
||||
// Firstly, check if we are containerized.
|
||||
if ok, err := IsContainerized(s.fs); err != nil || !ok {
|
||||
return nil, nil //nolint: nilnil
|
||||
}
|
||||
|
||||
total, err := s.cGroupCPUTotal()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get total cpu: %w", err)
|
||||
}
|
||||
|
||||
used1, err := s.cGroupCPUUsed()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
|
||||
}
|
||||
|
||||
// The measurements in /sys/fs/cgroup are counters.
|
||||
// We need to wait for a bit to get a difference.
|
||||
// Note that someone could reset the counter in the meantime.
|
||||
// We can't do anything about that.
|
||||
s.wait(s.sampleInterval)
|
||||
|
||||
used2, err := s.cGroupCPUUsed()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
|
||||
}
|
||||
|
||||
if used2 < used1 {
|
||||
// Someone reset the counter. Best we can do is count from zero.
|
||||
used1 = 0
|
||||
}
|
||||
|
||||
r := &Result{
|
||||
Unit: "cores",
|
||||
Used: used2 - used1,
|
||||
Total: ptr.To(total),
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupCPUTotal() (used float64, err error) {
|
||||
if s.isCGroupV2() {
|
||||
return s.cGroupV2CPUTotal()
|
||||
}
|
||||
|
||||
// Fall back to CGroupv1
|
||||
return s.cGroupV1CPUTotal()
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupCPUUsed() (used float64, err error) {
|
||||
if s.isCGroupV2() {
|
||||
return s.cGroupV2CPUUsed()
|
||||
}
|
||||
|
||||
return s.cGroupV1CPUUsed()
|
||||
}
|
||||
|
||||
func (s *Statter) isCGroupV2() bool {
|
||||
// Check for the presence of /sys/fs/cgroup/cpu.max
|
||||
_, err := s.fs.Stat(cgroupV2CPUMax)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupV2CPUUsed() (used float64, err error) {
|
||||
usageUs, err := readInt64Prefix(s.fs, cgroupV2CPUStat, "usage_usec")
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get cgroupv2 cpu used: %w", err)
|
||||
}
|
||||
periodUs, err := readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 1)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get cpu period: %w", err)
|
||||
}
|
||||
|
||||
return float64(usageUs) / float64(periodUs), nil
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupV2CPUTotal() (total float64, err error) {
|
||||
var quotaUs, periodUs int64
|
||||
periodUs, err = readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 1)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get cpu period: %w", err)
|
||||
}
|
||||
|
||||
quotaUs, err = readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 0)
|
||||
if err != nil {
|
||||
// Fall back to number of cores
|
||||
quotaUs = int64(s.nproc) * periodUs
|
||||
}
|
||||
|
||||
return float64(quotaUs) / float64(periodUs), nil
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupV1CPUTotal() (float64, error) {
|
||||
periodUs, err := readInt64(s.fs, cgroupV1CFSPeriodUs)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("read cpu period: %w", err)
|
||||
}
|
||||
|
||||
quotaUs, err := readInt64(s.fs, cgroupV1CFSQuotaUs)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("read cpu quota: %w", err)
|
||||
}
|
||||
|
||||
if quotaUs < 0 {
|
||||
// Fall back to the number of cores
|
||||
quotaUs = int64(s.nproc) * periodUs
|
||||
}
|
||||
|
||||
return float64(quotaUs) / float64(periodUs), nil
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupV1CPUUsed() (float64, error) {
|
||||
usageNs, err := readInt64(s.fs, cgroupV1CPUAcctUsage)
|
||||
if err != nil {
|
||||
// try alternate path
|
||||
usageNs, err = readInt64(s.fs, cgroupV1CPUAcctUsageAlt)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("read cpu used: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// usage is in ns, convert to us
|
||||
usageNs /= 1000
|
||||
periodUs, err := readInt64(s.fs, cgroupV1CFSPeriodUs)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get cpu period: %w", err)
|
||||
}
|
||||
|
||||
return float64(usageNs) / float64(periodUs), nil
|
||||
}
|
||||
|
||||
// ContainerMemory returns the memory usage of the container cgroup.
|
||||
// If the system is not containerized, this always returns nil.
|
||||
func (s *Statter) ContainerMemory() (*Result, error) {
|
||||
if ok, err := IsContainerized(s.fs); err != nil || !ok {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
if s.isCGroupV2() {
|
||||
return s.cGroupV2Memory()
|
||||
}
|
||||
|
||||
// Fall back to CGroupv1
|
||||
return s.cGroupV1Memory()
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupV2Memory() (*Result, error) {
|
||||
maxUsageBytes, err := readInt64(s.fs, cgroupV2MemoryMaxBytes)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read memory total: %w", err)
|
||||
}
|
||||
|
||||
currUsageBytes, err := readInt64(s.fs, cgroupV2MemoryUsageBytes)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read memory usage: %w", err)
|
||||
}
|
||||
|
||||
inactiveFileBytes, err := readInt64Prefix(s.fs, cgroupV2MemoryStat, "inactive_file")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read memory stats: %w", err)
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Total: ptr.To(float64(maxUsageBytes)),
|
||||
Used: float64(currUsageBytes - inactiveFileBytes),
|
||||
Unit: "B",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Statter) cGroupV1Memory() (*Result, error) {
|
||||
maxUsageBytes, err := readInt64(s.fs, cgroupV1MemoryMaxUsageBytes)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read memory total: %w", err)
|
||||
}
|
||||
|
||||
// need a space after total_rss so we don't hit something else
|
||||
usageBytes, err := readInt64(s.fs, cgroupV1MemoryUsageBytes)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read memory usage: %w", err)
|
||||
}
|
||||
|
||||
totalInactiveFileBytes, err := readInt64Prefix(s.fs, cgroupV1MemoryStat, "total_inactive_file")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read memory stats: %w", err)
|
||||
}
|
||||
|
||||
// Total memory used is usage - total_inactive_file
|
||||
return &Result{
|
||||
Total: ptr.To(float64(maxUsageBytes)),
|
||||
Used: float64(usageBytes - totalInactiveFileBytes),
|
||||
Unit: "B",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// read an int64 value from path
|
||||
func readInt64(fs afero.Fs, path string) (int64, error) {
|
||||
data, err := afero.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(string(bytes.TrimSpace(data)), 10, 64)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// read an int64 value from path at field idx separated by sep
|
||||
func readInt64SepIdx(fs afero.Fs, path, sep string, idx int) (int64, error) {
|
||||
data, err := afero.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
parts := strings.Split(string(data), sep)
|
||||
if len(parts) < idx {
|
||||
return 0, xerrors.Errorf("expected line %q to have at least %d parts", string(data), idx+1)
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(strings.TrimSpace(parts[idx]), 10, 64)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// read the first int64 value from path prefixed with prefix
|
||||
func readInt64Prefix(fs afero.Fs, path, prefix string) (int64, error) {
|
||||
data, err := afero.ReadFile(fs, path)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
|
||||
scn := bufio.NewScanner(bytes.NewReader(data))
|
||||
for scn.Scan() {
|
||||
line := scn.Text()
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) != 2 {
|
||||
return 0, xerrors.Errorf("parse %s: expected two fields but got %s", path, line)
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return 0, xerrors.Errorf("parse %s: did not find line with prefix %s", path, prefix)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package clistat
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
procMounts = "/proc/mounts"
|
||||
procOneCgroup = "/proc/1/cgroup"
|
||||
)
|
||||
|
||||
// IsContainerized returns whether the host is containerized.
|
||||
// This is adapted from https://github.com/elastic/go-sysinfo/tree/main/providers/linux/container.go#L31
|
||||
// with modifications to support Sysbox containers.
|
||||
// On non-Linux platforms, it always returns false.
|
||||
func IsContainerized(fs afero.Fs) (ok bool, err error) {
|
||||
cgData, err := afero.ReadFile(fs, procOneCgroup)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, xerrors.Errorf("read file %s: %w", procOneCgroup, err)
|
||||
}
|
||||
|
||||
scn := bufio.NewScanner(bytes.NewReader(cgData))
|
||||
for scn.Scan() {
|
||||
line := scn.Bytes()
|
||||
if bytes.Contains(line, []byte("docker")) ||
|
||||
bytes.Contains(line, []byte(".slice")) ||
|
||||
bytes.Contains(line, []byte("lxc")) ||
|
||||
bytes.Contains(line, []byte("kubepods")) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Last-ditch effort to detect Sysbox containers.
|
||||
// Check if we have anything mounted as type sysboxfs in /proc/mounts
|
||||
mountsData, err := afero.ReadFile(fs, procMounts)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, xerrors.Errorf("read file %s: %w", procMounts, err)
|
||||
}
|
||||
|
||||
scn = bufio.NewScanner(bytes.NewReader(mountsData))
|
||||
for scn.Scan() {
|
||||
line := scn.Bytes()
|
||||
if bytes.Contains(line, []byte("sysboxfs")) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we are _probably_ not running in a container.
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//go:build !windows
|
||||
|
||||
package clistat
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Disk returns the disk usage of the given path.
|
||||
// If path is empty, it returns the usage of the root directory.
|
||||
func (*Statter) Disk(path string) (*Result, error) {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var r Result
|
||||
r.Total = ptr.To(float64(stat.Blocks * uint64(stat.Bsize)))
|
||||
r.Used = float64(stat.Blocks-stat.Bfree) * float64(stat.Bsize)
|
||||
r.Unit = "B"
|
||||
return &r, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package clistat
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Disk returns the disk usage of the given path.
|
||||
// If path is empty, it defaults to C:\
|
||||
func (*Statter) Disk(path string) (*Result, error) {
|
||||
if path == "" {
|
||||
path = `C:\`
|
||||
}
|
||||
|
||||
pathPtr, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var freeBytes, totalBytes, availBytes uint64
|
||||
if err := windows.GetDiskFreeSpaceEx(
|
||||
pathPtr,
|
||||
&freeBytes,
|
||||
&totalBytes,
|
||||
&availBytes,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r Result
|
||||
r.Total = ptr.To(float64(totalBytes))
|
||||
r.Used = float64(totalBytes - freeBytes)
|
||||
r.Unit = "B"
|
||||
return &r, nil
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package clistat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/elastic/go-sysinfo"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/xerrors"
|
||||
"tailscale.com/types/ptr"
|
||||
|
||||
sysinfotypes "github.com/elastic/go-sysinfo/types"
|
||||
)
|
||||
|
||||
// Result is a generic result type for a statistic.
|
||||
// Total is the total amount of the resource available.
|
||||
// It is nil if the resource is not a finite quantity.
|
||||
// Unit is the unit of the resource.
|
||||
// Used is the amount of the resource used.
|
||||
type Result struct {
|
||||
Total *float64 `json:"total"`
|
||||
Unit string `json:"unit"`
|
||||
Used float64 `json:"used"`
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the result.
|
||||
func (r *Result) String() string {
|
||||
if r == nil {
|
||||
return "-"
|
||||
}
|
||||
|
||||
var usedDisplay, totalDisplay string
|
||||
var usedScaled, totalScaled float64
|
||||
var usedPrefix, totalPrefix string
|
||||
usedScaled, usedPrefix = humanize.ComputeSI(r.Used)
|
||||
usedDisplay = humanizeFloat(usedScaled)
|
||||
if r.Total != (*float64)(nil) {
|
||||
totalScaled, totalPrefix = humanize.ComputeSI(*r.Total)
|
||||
totalDisplay = humanizeFloat(totalScaled)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
_, _ = sb.WriteString(usedDisplay)
|
||||
|
||||
// If the unit prefixes of the used and total values are different,
|
||||
// display the used value's prefix to avoid confusion.
|
||||
if usedPrefix != totalPrefix || totalDisplay == "" {
|
||||
_, _ = sb.WriteString(" ")
|
||||
_, _ = sb.WriteString(usedPrefix)
|
||||
_, _ = sb.WriteString(r.Unit)
|
||||
}
|
||||
|
||||
if totalDisplay != "" {
|
||||
_, _ = sb.WriteString("/")
|
||||
_, _ = sb.WriteString(totalDisplay)
|
||||
_, _ = sb.WriteString(" ")
|
||||
_, _ = sb.WriteString(totalPrefix)
|
||||
_, _ = sb.WriteString(r.Unit)
|
||||
}
|
||||
|
||||
if r.Total != nil && *r.Total != 0.0 {
|
||||
_, _ = sb.WriteString(" (")
|
||||
_, _ = sb.WriteString(fmt.Sprintf("%.0f", r.Used/(*r.Total)*100.0))
|
||||
_, _ = sb.WriteString("%)")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(sb.String())
|
||||
}
|
||||
|
||||
func humanizeFloat(f float64) string {
|
||||
// humanize.FtoaWithDigits does not round correctly.
|
||||
prec := precision(f)
|
||||
rat := math.Pow(10, float64(prec))
|
||||
rounded := math.Round(f*rat) / rat
|
||||
return strconv.FormatFloat(rounded, 'f', -1, 64)
|
||||
}
|
||||
|
||||
// limit precision to 3 digits at most to preserve space
|
||||
func precision(f float64) int {
|
||||
fabs := math.Abs(f)
|
||||
if fabs == 0.0 {
|
||||
return 0
|
||||
}
|
||||
if fabs < 1.0 {
|
||||
return 3
|
||||
}
|
||||
if fabs < 10.0 {
|
||||
return 2
|
||||
}
|
||||
if fabs < 100.0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Statter is a system statistics collector.
|
||||
// It is a thin wrapper around the elastic/go-sysinfo library.
|
||||
type Statter struct {
|
||||
hi sysinfotypes.Host
|
||||
fs afero.Fs
|
||||
sampleInterval time.Duration
|
||||
nproc int
|
||||
wait func(time.Duration)
|
||||
}
|
||||
|
||||
type Option func(*Statter)
|
||||
|
||||
// WithSampleInterval sets the sample interval for the statter.
|
||||
func WithSampleInterval(d time.Duration) Option {
|
||||
return func(s *Statter) {
|
||||
s.sampleInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithFS sets the fs for the statter.
|
||||
func WithFS(fs afero.Fs) Option {
|
||||
return func(s *Statter) {
|
||||
s.fs = fs
|
||||
}
|
||||
}
|
||||
|
||||
func New(opts ...Option) (*Statter, error) {
|
||||
hi, err := sysinfo.Host()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get host info: %w", err)
|
||||
}
|
||||
s := &Statter{
|
||||
hi: hi,
|
||||
fs: afero.NewReadOnlyFs(afero.NewOsFs()),
|
||||
sampleInterval: 100 * time.Millisecond,
|
||||
nproc: runtime.NumCPU(),
|
||||
wait: func(d time.Duration) {
|
||||
<-time.After(d)
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// HostCPU returns the CPU usage of the host. This is calculated by
|
||||
// taking two samples of CPU usage and calculating the difference.
|
||||
// Total will always be equal to the number of cores.
|
||||
// Used will be an estimate of the number of cores used during the sample interval.
|
||||
// This is calculated by taking the difference between the total and idle HostCPU time
|
||||
// and scaling it by the number of cores.
|
||||
// Units are in "cores".
|
||||
func (s *Statter) HostCPU() (*Result, error) {
|
||||
r := &Result{
|
||||
Unit: "cores",
|
||||
Total: ptr.To(float64(s.nproc)),
|
||||
}
|
||||
c1, err := s.hi.CPUTime()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get first cpu sample: %w", err)
|
||||
}
|
||||
s.wait(s.sampleInterval)
|
||||
c2, err := s.hi.CPUTime()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get second cpu sample: %w", err)
|
||||
}
|
||||
total := c2.Total() - c1.Total()
|
||||
if total == 0 {
|
||||
return r, nil // no change
|
||||
}
|
||||
idle := c2.Idle - c1.Idle
|
||||
used := total - idle
|
||||
scaleFactor := float64(s.nproc) / total.Seconds()
|
||||
r.Used = used.Seconds() * scaleFactor
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// HostMemory returns the memory usage of the host, in gigabytes.
|
||||
func (s *Statter) HostMemory() (*Result, error) {
|
||||
r := &Result{
|
||||
Unit: "B",
|
||||
}
|
||||
hm, err := s.hi.Memory()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get memory info: %w", err)
|
||||
}
|
||||
r.Total = ptr.To(float64(hm.Total))
|
||||
r.Used = float64(hm.Used)
|
||||
return r, nil
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package clistat
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestResultString(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tt := range []struct {
|
||||
Expected string
|
||||
Result Result
|
||||
}{
|
||||
{
|
||||
Expected: "1.23/5.68 quatloos (22%)",
|
||||
Result: Result{Used: 1.234, Total: ptr.To(5.678), Unit: "quatloos"},
|
||||
},
|
||||
{
|
||||
Expected: "0/0 HP",
|
||||
Result: Result{Used: 0.0, Total: ptr.To(0.0), Unit: "HP"},
|
||||
},
|
||||
{
|
||||
Expected: "123 seconds",
|
||||
Result: Result{Used: 123.01, Total: nil, Unit: "seconds"},
|
||||
},
|
||||
{
|
||||
Expected: "12.3",
|
||||
Result: Result{Used: 12.34, Total: nil, Unit: ""},
|
||||
},
|
||||
{
|
||||
Expected: "1.54 kB",
|
||||
Result: Result{Used: 1536, Total: nil, Unit: "B"},
|
||||
},
|
||||
{
|
||||
Expected: "1.23 things",
|
||||
Result: Result{Used: 1.234, Total: nil, Unit: "things"},
|
||||
},
|
||||
{
|
||||
Expected: "1 B/100 TB (0%)",
|
||||
Result: Result{Used: 1, Total: ptr.To(1000 * 1000 * 1000 * 1000 * 100.0), Unit: "B"},
|
||||
},
|
||||
{
|
||||
Expected: "500 mcores/8 cores (6%)",
|
||||
Result: Result{Used: 0.5, Total: ptr.To(8.0), Unit: "cores"},
|
||||
},
|
||||
} {
|
||||
assert.Equal(t, tt.Expected, tt.Result.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// We cannot make many assertions about the data we get back
|
||||
// for host-specific measurements because these tests could
|
||||
// and should run successfully on any OS.
|
||||
// The best we can do is assert that it is non-zero.
|
||||
t.Run("HostOnly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, fsHostOnly)
|
||||
s, err := New(WithFS(fs))
|
||||
require.NoError(t, err)
|
||||
t.Run("HostCPU", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cpu, err := s.HostCPU()
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, cpu.Used)
|
||||
assert.NotZero(t, cpu.Total)
|
||||
assert.Equal(t, "cores", cpu.Unit)
|
||||
})
|
||||
|
||||
t.Run("HostMemory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mem, err := s.HostMemory()
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, mem.Used)
|
||||
assert.NotZero(t, mem.Total)
|
||||
assert.Equal(t, "B", mem.Unit)
|
||||
})
|
||||
|
||||
t.Run("HostDisk", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
disk, err := s.Disk("") // default to home dir
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, disk.Used)
|
||||
assert.NotZero(t, disk.Total)
|
||||
assert.Equal(t, "B", disk.Unit)
|
||||
})
|
||||
})
|
||||
|
||||
// Sometimes we do need to "fake" some stuff
|
||||
// that happens while we wait.
|
||||
withWait := func(waitF func(time.Duration)) Option {
|
||||
return func(s *Statter) {
|
||||
s.wait = waitF
|
||||
}
|
||||
}
|
||||
|
||||
// Other times we just want things to run fast.
|
||||
withNoWait := func(s *Statter) {
|
||||
s.wait = func(time.Duration) {}
|
||||
}
|
||||
|
||||
// We don't want to use the actual host CPU here.
|
||||
withNproc := func(n int) Option {
|
||||
return func(s *Statter) {
|
||||
s.nproc = n
|
||||
}
|
||||
}
|
||||
|
||||
// For container-specific measurements, everything we need
|
||||
// can be read from the filesystem. We control the FS, so
|
||||
// we control the data.
|
||||
t.Run("CGroupV1", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ContainerCPU/Limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, fsContainerCgroupV1)
|
||||
fakeWait := func(time.Duration) {
|
||||
// Fake 1 second in ns of usage
|
||||
mungeFS(t, fs, cgroupV1CPUAcctUsage, "100000000")
|
||||
}
|
||||
s, err := New(WithFS(fs), withWait(fakeWait))
|
||||
require.NoError(t, err)
|
||||
cpu, err := s.ContainerCPU()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cpu)
|
||||
assert.Equal(t, 1.0, cpu.Used)
|
||||
require.NotNil(t, cpu.Total)
|
||||
assert.Equal(t, 2.5, *cpu.Total)
|
||||
assert.Equal(t, "cores", cpu.Unit)
|
||||
})
|
||||
|
||||
t.Run("ContainerCPU/NoLimit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, fsContainerCgroupV1NoLimit)
|
||||
fakeWait := func(time.Duration) {
|
||||
// Fake 1 second in ns of usage
|
||||
mungeFS(t, fs, cgroupV1CPUAcctUsage, "100000000")
|
||||
}
|
||||
s, err := New(WithFS(fs), withNproc(2), withWait(fakeWait))
|
||||
require.NoError(t, err)
|
||||
cpu, err := s.ContainerCPU()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cpu)
|
||||
assert.Equal(t, 1.0, cpu.Used)
|
||||
require.NotNil(t, cpu.Total)
|
||||
assert.Equal(t, 2.0, *cpu.Total)
|
||||
assert.Equal(t, "cores", cpu.Unit)
|
||||
})
|
||||
|
||||
t.Run("ContainerMemory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, fsContainerCgroupV1)
|
||||
s, err := New(WithFS(fs), withNoWait)
|
||||
require.NoError(t, err)
|
||||
mem, err := s.ContainerMemory()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, mem)
|
||||
assert.Equal(t, 268435456.0, mem.Used)
|
||||
assert.NotNil(t, mem.Total)
|
||||
assert.Equal(t, 1073741824.0, *mem.Total)
|
||||
assert.Equal(t, "B", mem.Unit)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CGroupV2", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("ContainerCPU/Limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, fsContainerCgroupV2)
|
||||
fakeWait := func(time.Duration) {
|
||||
mungeFS(t, fs, cgroupV2CPUStat, "usage_usec 100000")
|
||||
}
|
||||
s, err := New(WithFS(fs), withWait(fakeWait))
|
||||
require.NoError(t, err)
|
||||
cpu, err := s.ContainerCPU()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cpu)
|
||||
assert.Equal(t, 1.0, cpu.Used)
|
||||
require.NotNil(t, cpu.Total)
|
||||
assert.Equal(t, 2.5, *cpu.Total)
|
||||
assert.Equal(t, "cores", cpu.Unit)
|
||||
})
|
||||
|
||||
t.Run("ContainerCPU/NoLimit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, fsContainerCgroupV2NoLimit)
|
||||
fakeWait := func(time.Duration) {
|
||||
mungeFS(t, fs, cgroupV2CPUStat, "usage_usec 100000")
|
||||
}
|
||||
s, err := New(WithFS(fs), withNproc(2), withWait(fakeWait))
|
||||
require.NoError(t, err)
|
||||
cpu, err := s.ContainerCPU()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cpu)
|
||||
assert.Equal(t, 1.0, cpu.Used)
|
||||
require.NotNil(t, cpu.Total)
|
||||
assert.Equal(t, 2.0, *cpu.Total)
|
||||
assert.Equal(t, "cores", cpu.Unit)
|
||||
})
|
||||
|
||||
t.Run("ContainerMemory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, fsContainerCgroupV2)
|
||||
s, err := New(WithFS(fs), withNoWait)
|
||||
require.NoError(t, err)
|
||||
mem, err := s.ContainerMemory()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, mem)
|
||||
assert.Equal(t, 268435456.0, mem.Used)
|
||||
assert.NotNil(t, mem.Total)
|
||||
assert.Equal(t, 1073741824.0, *mem.Total)
|
||||
assert.Equal(t, "B", mem.Unit)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsContainerized(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
Name string
|
||||
FS map[string]string
|
||||
Expected bool
|
||||
Error string
|
||||
}{
|
||||
{
|
||||
Name: "Empty",
|
||||
FS: map[string]string{},
|
||||
Expected: false,
|
||||
Error: "",
|
||||
},
|
||||
{
|
||||
Name: "BareMetal",
|
||||
FS: fsHostOnly,
|
||||
Expected: false,
|
||||
Error: "",
|
||||
},
|
||||
{
|
||||
Name: "Docker",
|
||||
FS: fsContainerCgroupV1,
|
||||
Expected: true,
|
||||
Error: "",
|
||||
},
|
||||
{
|
||||
Name: "Sysbox",
|
||||
FS: fsContainerSysbox,
|
||||
Expected: true,
|
||||
Error: "",
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := initFS(t, tt.FS)
|
||||
actual, err := IsContainerized(fs)
|
||||
if tt.Error == "" {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.Expected, actual)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, tt.Error)
|
||||
assert.False(t, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helper function for initializing a fs
|
||||
func initFS(t testing.TB, m map[string]string) afero.Fs {
|
||||
t.Helper()
|
||||
fs := afero.NewMemMapFs()
|
||||
for k, v := range m {
|
||||
mungeFS(t, fs, k, v)
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// helper function for writing v to fs under path k
|
||||
func mungeFS(t testing.TB, fs afero.Fs, k, v string) {
|
||||
t.Helper()
|
||||
require.NoError(t, afero.WriteFile(fs, k, []byte(v+"\n"), 0o600))
|
||||
}
|
||||
|
||||
var (
|
||||
fsHostOnly = map[string]string{
|
||||
procOneCgroup: "0::/",
|
||||
procMounts: "/dev/sda1 / ext4 rw,relatime 0 0",
|
||||
}
|
||||
fsContainerSysbox = map[string]string{
|
||||
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
|
||||
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
|
||||
sysboxfs /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
|
||||
cgroupV2CPUMax: "250000 100000",
|
||||
cgroupV2CPUStat: "usage_usec 0",
|
||||
}
|
||||
fsContainerCgroupV2 = map[string]string{
|
||||
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
|
||||
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
|
||||
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
|
||||
cgroupV2CPUMax: "250000 100000",
|
||||
cgroupV2CPUStat: "usage_usec 0",
|
||||
cgroupV2MemoryMaxBytes: "1073741824",
|
||||
cgroupV2MemoryUsageBytes: "536870912",
|
||||
cgroupV2MemoryStat: "inactive_file 268435456",
|
||||
}
|
||||
fsContainerCgroupV2NoLimit = map[string]string{
|
||||
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
|
||||
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
|
||||
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
|
||||
cgroupV2CPUMax: "max 100000",
|
||||
cgroupV2CPUStat: "usage_usec 0",
|
||||
cgroupV2MemoryMaxBytes: "1073741824",
|
||||
cgroupV2MemoryUsageBytes: "536870912",
|
||||
cgroupV2MemoryStat: "inactive_file 268435456",
|
||||
}
|
||||
fsContainerCgroupV1 = map[string]string{
|
||||
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
|
||||
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
|
||||
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
|
||||
cgroupV1CPUAcctUsage: "0",
|
||||
cgroupV1CFSQuotaUs: "250000",
|
||||
cgroupV1CFSPeriodUs: "100000",
|
||||
cgroupV1MemoryMaxUsageBytes: "1073741824",
|
||||
cgroupV1MemoryUsageBytes: "536870912",
|
||||
cgroupV1MemoryStat: "total_inactive_file 268435456",
|
||||
}
|
||||
fsContainerCgroupV1NoLimit = map[string]string{
|
||||
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
|
||||
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
|
||||
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
|
||||
cgroupV1CPUAcctUsage: "0",
|
||||
cgroupV1CFSQuotaUs: "-1",
|
||||
cgroupV1CFSPeriodUs: "100000",
|
||||
cgroupV1MemoryMaxUsageBytes: "1073741824",
|
||||
cgroupV1MemoryUsageBytes: "536870912",
|
||||
cgroupV1MemoryStat: "total_inactive_file 268435456",
|
||||
}
|
||||
)
|
||||
@@ -103,6 +103,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
|
||||
r.stop(),
|
||||
r.update(),
|
||||
r.restart(),
|
||||
r.stat(),
|
||||
|
||||
// Hidden
|
||||
r.gitssh(),
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/cli/clibase"
|
||||
"github.com/coder/coder/cli/clistat"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
)
|
||||
|
||||
func (r *RootCmd) stat() *clibase.Cmd {
|
||||
fs := afero.NewReadOnlyFs(afero.NewOsFs())
|
||||
defaultCols := []string{
|
||||
"host_cpu",
|
||||
"host_memory",
|
||||
"home_disk",
|
||||
"container_cpu",
|
||||
"container_memory",
|
||||
}
|
||||
formatter := cliui.NewOutputFormatter(
|
||||
cliui.TableFormat([]statsRow{}, defaultCols),
|
||||
cliui.JSONFormat(),
|
||||
)
|
||||
st, err := clistat.New(clistat.WithFS(fs))
|
||||
if err != nil {
|
||||
panic(xerrors.Errorf("initialize workspace stats collector: %w", err))
|
||||
}
|
||||
|
||||
cmd := &clibase.Cmd{
|
||||
Use: "stat",
|
||||
Short: "Show resource usage for the current workspace.",
|
||||
Children: []*clibase.Cmd{
|
||||
r.statCPU(st, fs),
|
||||
r.statMem(st, fs),
|
||||
r.statDisk(st),
|
||||
},
|
||||
Handler: func(inv *clibase.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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sr.HostMemory = ms
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ds, err := st.Disk(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()
|
||||
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(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
|
||||
var hostArg bool
|
||||
var prefixArg string
|
||||
formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
|
||||
cmd := &clibase.Cmd{
|
||||
Use: "cpu",
|
||||
Short: "Show CPU usage, in cores.",
|
||||
Options: clibase.OptionSet{
|
||||
{
|
||||
Flag: "host",
|
||||
Value: clibase.BoolOf(&hostArg),
|
||||
Description: "Force host CPU measurement.",
|
||||
},
|
||||
{
|
||||
Flag: "prefix",
|
||||
Value: clibase.StringOf(&prefixArg),
|
||||
Description: "Unit prefix.",
|
||||
Default: "",
|
||||
},
|
||||
},
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
var cs *clistat.Result
|
||||
var err error
|
||||
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
|
||||
cs, err = s.ContainerCPU()
|
||||
} else {
|
||||
cs, err = s.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(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
|
||||
var hostArg bool
|
||||
formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
|
||||
cmd := &clibase.Cmd{
|
||||
Use: "mem",
|
||||
Short: "Show memory usage, in gigabytes.",
|
||||
Options: clibase.OptionSet{
|
||||
{
|
||||
Flag: "host",
|
||||
Value: clibase.BoolOf(&hostArg),
|
||||
Description: "Force host memory measurement.",
|
||||
},
|
||||
},
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
var ms *clistat.Result
|
||||
var err error
|
||||
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
|
||||
ms, err = s.ContainerMemory()
|
||||
} else {
|
||||
ms, err = s.HostMemory()
|
||||
}
|
||||
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(s *clistat.Statter) *clibase.Cmd {
|
||||
var pathArg string
|
||||
formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
|
||||
cmd := &clibase.Cmd{
|
||||
Use: "disk",
|
||||
Short: "Show disk usage, in gigabytes.",
|
||||
Options: clibase.OptionSet{
|
||||
{
|
||||
Flag: "path",
|
||||
Value: clibase.StringOf(&pathArg),
|
||||
Description: "Path for which to check disk usage.",
|
||||
Default: "/",
|
||||
},
|
||||
},
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
ds, err := s.Disk(pathArg)
|
||||
if err != nil {
|
||||
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"`
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/clistat"
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
// This just tests that the stat command is recognized and does not output
|
||||
// an empty string. Actually testing the output of the stats command is
|
||||
// fraught with all sorts of fun. Some more detailed testing of the stats
|
||||
// output is performed in the tests in the clistat package.
|
||||
func TestStatCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "all", "--output=json")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
require.NotEmpty(t, s)
|
||||
// Must be valid JSON
|
||||
tmp := make([]clistat.Result, 0)
|
||||
require.NoError(t, json.NewDecoder(strings.NewReader(s)).Decode(&tmp))
|
||||
})
|
||||
t.Run("Table", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "all", "--output=table")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
require.NotEmpty(t, s)
|
||||
require.Contains(t, s, "HOST CPU")
|
||||
require.Contains(t, s, "HOST MEMORY")
|
||||
require.Contains(t, s, "HOME DISK")
|
||||
})
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "all")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
require.NotEmpty(t, s)
|
||||
require.Contains(t, s, "HOST CPU")
|
||||
require.Contains(t, s, "HOST MEMORY")
|
||||
require.Contains(t, s, "HOME DISK")
|
||||
})
|
||||
}
|
||||
|
||||
func TestStatCPUCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Text", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "cpu", "--output=text")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
require.NotEmpty(t, s)
|
||||
})
|
||||
|
||||
t.Run("JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "cpu", "--output=json")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
tmp := clistat.Result{}
|
||||
require.NoError(t, json.NewDecoder(strings.NewReader(s)).Decode(&tmp))
|
||||
require.NotZero(t, tmp.Used)
|
||||
require.NotNil(t, tmp.Total)
|
||||
require.NotZero(t, *tmp.Total)
|
||||
require.Equal(t, "cores", tmp.Unit)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStatMemCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Text", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "mem", "--output=text")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
require.NotEmpty(t, s)
|
||||
})
|
||||
|
||||
t.Run("JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "mem", "--output=json")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
tmp := clistat.Result{}
|
||||
require.NoError(t, json.NewDecoder(strings.NewReader(s)).Decode(&tmp))
|
||||
require.NotZero(t, tmp.Used)
|
||||
require.NotNil(t, tmp.Total)
|
||||
require.NotZero(t, *tmp.Total)
|
||||
require.Equal(t, "B", tmp.Unit)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStatDiskCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Text", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "disk", "--output=text")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
require.NotEmpty(t, s)
|
||||
})
|
||||
|
||||
t.Run("JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
t.Cleanup(cancel)
|
||||
inv, _ := clitest.New(t, "stat", "disk", "--output=json")
|
||||
buf := new(bytes.Buffer)
|
||||
inv.Stdout = buf
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
s := buf.String()
|
||||
tmp := clistat.Result{}
|
||||
require.NoError(t, json.NewDecoder(strings.NewReader(s)).Decode(&tmp))
|
||||
require.NotZero(t, tmp.Used)
|
||||
require.NotNil(t, tmp.Total)
|
||||
require.NotZero(t, *tmp.Total)
|
||||
require.Equal(t, "B", tmp.Unit)
|
||||
})
|
||||
}
|
||||
Vendored
+1
@@ -34,6 +34,7 @@ Coder v0.0.0-devel — A tool for provisioning self-hosted development environme
|
||||
workspace
|
||||
ssh Start a shell into a workspace
|
||||
start Start a workspace
|
||||
stat Show resource usage for the current workspace.
|
||||
state Manually manage Terraform state to fix broken workspaces
|
||||
stop Stop a workspace
|
||||
templates Manage templates
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Usage: coder stat [flags]
|
||||
|
||||
Show resource usage for the current workspace.
|
||||
|
||||
[1mSubcommands[0m
|
||||
cpu Show CPU usage, in cores.
|
||||
disk Show disk usage, in gigabytes.
|
||||
mem Show memory usage, in gigabytes.
|
||||
|
||||
[1mOptions[0m
|
||||
-c, --column string-array (default: host_cpu,host_memory,home_disk,container_cpu,container_memory)
|
||||
Columns to display in table output. Available columns: host cpu, host
|
||||
memory, home disk, container cpu, container memory.
|
||||
|
||||
-o, --output string (default: table)
|
||||
Output format. Available formats: table, json.
|
||||
|
||||
---
|
||||
Run `coder --help` for a list of global options.
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
Usage: coder stat cpu [flags]
|
||||
|
||||
Show CPU usage, in cores.
|
||||
|
||||
[1mOptions[0m
|
||||
--host bool
|
||||
Force host CPU measurement.
|
||||
|
||||
-o, --output string (default: text)
|
||||
Output format. Available formats: text, json.
|
||||
|
||||
--prefix string
|
||||
Unit prefix.
|
||||
|
||||
---
|
||||
Run `coder --help` for a list of global options.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Usage: coder stat disk [flags]
|
||||
|
||||
Show disk usage, in gigabytes.
|
||||
|
||||
[1mOptions[0m
|
||||
-o, --output string (default: text)
|
||||
Output format. Available formats: text, json.
|
||||
|
||||
--path string (default: /)
|
||||
Path for which to check disk usage.
|
||||
|
||||
---
|
||||
Run `coder --help` for a list of global options.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Usage: coder stat mem [flags]
|
||||
|
||||
Show memory usage, in gigabytes.
|
||||
|
||||
[1mOptions[0m
|
||||
--host bool
|
||||
Force host memory measurement.
|
||||
|
||||
-o, --output string (default: text)
|
||||
Output format. Available formats: text, json.
|
||||
|
||||
---
|
||||
Run `coder --help` for a list of global options.
|
||||
@@ -49,6 +49,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
|
||||
| [<code>speedtest</code>](./cli/speedtest.md) | Run upload and download tests from your machine to a workspace |
|
||||
| [<code>ssh</code>](./cli/ssh.md) | Start a shell into a workspace |
|
||||
| [<code>start</code>](./cli/start.md) | Start a workspace |
|
||||
| [<code>stat</code>](./cli/stat.md) | Show resource usage for the current workspace. |
|
||||
| [<code>state</code>](./cli/state.md) | Manually manage Terraform state to fix broken workspaces |
|
||||
| [<code>stop</code>](./cli/stop.md) | Stop a workspace |
|
||||
| [<code>templates</code>](./cli/templates.md) | Manage templates |
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
|
||||
# stat
|
||||
|
||||
Show resource usage for the current workspace.
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder stat [flags]
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Name | Purpose |
|
||||
| ----------------------------------- | -------------------------------- |
|
||||
| [<code>cpu</code>](./stat_cpu.md) | Show CPU usage, in cores. |
|
||||
| [<code>disk</code>](./stat_disk.md) | Show disk usage, in gigabytes. |
|
||||
| [<code>mem</code>](./stat_mem.md) | Show memory usage, in gigabytes. |
|
||||
|
||||
## Options
|
||||
|
||||
### -c, --column
|
||||
|
||||
| | |
|
||||
| ------- | -------------------------------------------------------------------------- |
|
||||
| Type | <code>string-array</code> |
|
||||
| Default | <code>host_cpu,host_memory,home_disk,container_cpu,container_memory</code> |
|
||||
|
||||
Columns to display in table output. Available columns: host cpu, host memory, home disk, container cpu, container memory.
|
||||
|
||||
### -o, --output
|
||||
|
||||
| | |
|
||||
| ------- | ------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Default | <code>table</code> |
|
||||
|
||||
Output format. Available formats: table, json.
|
||||
@@ -0,0 +1,38 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
|
||||
# stat cpu
|
||||
|
||||
Show CPU usage, in cores.
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder stat cpu [flags]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### --host
|
||||
|
||||
| | |
|
||||
| ---- | ----------------- |
|
||||
| Type | <code>bool</code> |
|
||||
|
||||
Force host CPU measurement.
|
||||
|
||||
### -o, --output
|
||||
|
||||
| | |
|
||||
| ------- | ------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Default | <code>text</code> |
|
||||
|
||||
Output format. Available formats: text, json.
|
||||
|
||||
### --prefix
|
||||
|
||||
| | |
|
||||
| ---- | ------------------- |
|
||||
| Type | <code>string</code> |
|
||||
|
||||
Unit prefix.
|
||||
@@ -0,0 +1,31 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
|
||||
# stat disk
|
||||
|
||||
Show disk usage, in gigabytes.
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder stat disk [flags]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### -o, --output
|
||||
|
||||
| | |
|
||||
| ------- | ------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Default | <code>text</code> |
|
||||
|
||||
Output format. Available formats: text, json.
|
||||
|
||||
### --path
|
||||
|
||||
| | |
|
||||
| ------- | ------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Default | <code>/</code> |
|
||||
|
||||
Path for which to check disk usage.
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
|
||||
# stat mem
|
||||
|
||||
Show memory usage, in gigabytes.
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder stat mem [flags]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### --host
|
||||
|
||||
| | |
|
||||
| ---- | ----------------- |
|
||||
| Type | <code>bool</code> |
|
||||
|
||||
Force host memory measurement.
|
||||
|
||||
### -o, --output
|
||||
|
||||
| | |
|
||||
| ------- | ------------------- |
|
||||
| Type | <code>string</code> |
|
||||
| Default | <code>text</code> |
|
||||
|
||||
Output format. Available formats: text, json.
|
||||
@@ -703,6 +703,26 @@
|
||||
"description": "Start a workspace",
|
||||
"path": "cli/start.md"
|
||||
},
|
||||
{
|
||||
"title": "stat",
|
||||
"description": "Show resource usage for the current workspace.",
|
||||
"path": "cli/stat.md"
|
||||
},
|
||||
{
|
||||
"title": "stat cpu",
|
||||
"description": "Show CPU usage, in cores.",
|
||||
"path": "cli/stat_cpu.md"
|
||||
},
|
||||
{
|
||||
"title": "stat disk",
|
||||
"description": "Show disk usage, in gigabytes.",
|
||||
"path": "cli/stat_disk.md"
|
||||
},
|
||||
{
|
||||
"title": "stat mem",
|
||||
"description": "Show memory usage, in gigabytes.",
|
||||
"path": "cli/stat_mem.md"
|
||||
},
|
||||
{
|
||||
"title": "state",
|
||||
"description": "Manually manage Terraform state to fix broken workspaces",
|
||||
|
||||
Vendored
+34
-12
@@ -16,6 +16,10 @@ See the [Terraform reference](https://registry.terraform.io/providers/coder/code
|
||||
All of these examples use [heredoc strings](https://developer.hashicorp.com/terraform/language/expressions/strings#heredoc-strings) for the script declaration. With heredoc strings, you
|
||||
can script without messy escape codes, just as if you were working in your terminal.
|
||||
|
||||
Some of the below examples use the [`coder stat`](../cli/stat.md) command.
|
||||
This is useful for determining CPU/memory usage inside a container, which
|
||||
can be tricky otherwise.
|
||||
|
||||
Here's a standard set of metadata snippets for Linux agents:
|
||||
|
||||
```hcl
|
||||
@@ -25,10 +29,38 @@ resource "coder_agent" "main" {
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "cpu"
|
||||
# Uses the coder stat command to get container CPU usage.
|
||||
script = "coder stat cpu"
|
||||
interval = 1
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage"
|
||||
key = "mem"
|
||||
# Uses the coder stat command to get container memory usage in GiB.
|
||||
script = "coder stat mem --prefix Gi"
|
||||
interval = 1
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "cpu_host"
|
||||
# calculates CPU usage by summing the "us", "sy" and "id" columns of
|
||||
# vmstat.
|
||||
# top.
|
||||
script = <<EOT
|
||||
top -bn1 | awk 'FNR==3 {printf "%2.0f%%", $2+$3+$4}'
|
||||
top -bn1 | awk 'FNR==3 {printf "%2.0f%%", $2+$3+$4}'
|
||||
EOT
|
||||
interval = 1
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "mem_host"
|
||||
script = <<EOT
|
||||
free | awk '/^Mem/ { printf("%.0f%%", $4/$2 * 100.0) }'
|
||||
EOT
|
||||
interval = 1
|
||||
timeout = 1
|
||||
@@ -42,16 +74,6 @@ resource "coder_agent" "main" {
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage"
|
||||
key = "mem"
|
||||
script = <<EOT
|
||||
free | awk '/^Mem/ { printf("%.0f%%", $4/$2 * 100.0) }'
|
||||
EOT
|
||||
interval = 1
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average"
|
||||
key = "load"
|
||||
|
||||
+27
-60
@@ -93,106 +93,73 @@ resource "coder_agent" "dev" {
|
||||
}
|
||||
startup_script_behavior = "blocking"
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
key = "0_cpu_usage"
|
||||
script = <<EOT
|
||||
#!/bin/bash
|
||||
# check if we are in cgroup v2 or v1
|
||||
if [ -e /sys/fs/cgroup/cpu.stat ]; then
|
||||
# cgroup v2
|
||||
cusage=$(cat /sys/fs/cgroup/cpu.stat | head -n 1 | awk '{ print $2 }')
|
||||
else
|
||||
# cgroup v1
|
||||
# cusage=$(cat /sys/fs/cgroup/cpu,cpuacct/cpuacct.usage)
|
||||
echo "Coming Soon!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# get previous usage
|
||||
if [ -e /tmp/cusage ]; then
|
||||
cusage_p=$(cat /tmp/cusage)
|
||||
else
|
||||
echo $cusage > /tmp/cusage
|
||||
echo "Unknown"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# interval in microseconds should be metadata.interval * 1000000
|
||||
interval=10000000
|
||||
ncores=$(nproc)
|
||||
echo "$cusage $cusage_p $interval $ncores" | awk '{ printf "%2.0f%%\n", (($1 - $2)/$3/$4)*100 }'
|
||||
|
||||
EOT
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
key = "1_ram_usage"
|
||||
script = <<EOT
|
||||
#!/bin/bash
|
||||
# first check if we are in cgroup v2 or v1
|
||||
if [ -e /sys/fs/cgroup/memory.stat ]; then
|
||||
# cgroup v2
|
||||
echo "`cat /sys/fs/cgroup/memory.current` `cat /sys/fs/cgroup/memory.max`" | awk '{ used=$1/1024/1024/1024; total=$2/1024/1024/1024; printf "%0.2f / %0.2f GB\n", used, total }'
|
||||
else
|
||||
# cgroup v1
|
||||
echo "`cat /sys/fs/cgroup/memory/memory.usage_in_bytes` `cat /sys/fs/cgroup/memory/memory.limit_in_bytes`" | awk '{ used=$1/1024/1024/1024; total=$2/1024/1024/1024; printf "%0.2f / %0.2f GB\n", used, total }'
|
||||
fi
|
||||
EOT
|
||||
}
|
||||
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "2_cpu_host"
|
||||
script = <<EOT
|
||||
vmstat | awk 'FNR==3 {printf "%2.0f%%", $13+$14+$16}'
|
||||
EOT
|
||||
key = "2_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory and Swap Usage (Host)"
|
||||
key = "3_mem_swap_host"
|
||||
display_name = "RAM Usage (Host)"
|
||||
key = "3_ram_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "4_swap_usage_host"
|
||||
script = <<EOT
|
||||
mem_usage=`free | awk '/^Mem/ { printf("%.0f%%", $3/$2 * 100.0) }'`
|
||||
swp_usage=`free | awk '/^Swap/ { printf("%.0f%%", $3/$2 * 100.0) }'`
|
||||
echo "Memory: $mem_usage, Swap: $swp_usage"
|
||||
EOT
|
||||
echo "$(free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }') GiB"
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "4_load_host"
|
||||
key = "5_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 10
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Disk Usage (Host)"
|
||||
key = "5_disk_host"
|
||||
script = <<EOT
|
||||
df --output=pcent / | sed -n "2p"
|
||||
EOT
|
||||
key = "6_disk_host"
|
||||
script = "coder stat disk --path /"
|
||||
interval = 600
|
||||
timeout = 10 #getting disk usage can take a while
|
||||
timeout = 10
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Word of the Day"
|
||||
key = "6_word"
|
||||
key = "7_word"
|
||||
script = <<EOT
|
||||
curl -o - --silent https://www.merriam-webster.com/word-of-the-day 2>&1 | awk ' $0 ~ "Word of the Day: [A-z]+" { print $5; exit }'
|
||||
EOT
|
||||
|
||||
@@ -46,6 +46,72 @@ resource "coder_agent" "main" {
|
||||
GIT_AUTHOR_EMAIL = "${data.coder_workspace.me.owner_email}"
|
||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace.me.owner_email}"
|
||||
}
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "code-server" {
|
||||
|
||||
@@ -116,6 +116,62 @@ resource "coder_agent" "main" {
|
||||
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server --version 4.11.0
|
||||
/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
|
||||
EOT
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# code-server
|
||||
|
||||
@@ -218,6 +218,7 @@ require (
|
||||
github.com/docker/docker v23.0.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elastic/go-windows v1.0.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
@@ -358,3 +359,16 @@ require (
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dave/dst v0.27.2
|
||||
github.com/gobwas/httphead v0.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
)
|
||||
|
||||
@@ -236,6 +236,7 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/go-sysinfo v1.11.0 h1:QW+6BF1oxBoAprH3w2yephF7xLkrrSXj7gl2xC2BM4w=
|
||||
github.com/elastic/go-sysinfo v1.11.0/go.mod h1:6KQb31j0QeWBDF88jIdWSxE8cwoOB9tO4Y4osN7Q70E=
|
||||
github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY=
|
||||
|
||||
Reference in New Issue
Block a user