├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go └── poller.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | *~ 3 | /Dockerfile 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /mstat 3 | /.idea 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # docker build -t bpowers/mstat . 2 | FROM golang:1.15 as builder 3 | MAINTAINER Bobby Powers 4 | 5 | WORKDIR /go/src/github.com/bpowers/mstat 6 | COPY . . 7 | 8 | RUN make \ 9 | && make install PREFIX=/usr/local 10 | 11 | 12 | FROM alpine:3 13 | 14 | COPY --from=builder /usr/local/bin/mstat /usr/local/bin/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Bobby Powers 0 && args[0] == "--" { 125 | args = args[1:] 126 | } 127 | if len(args) < 1 { 128 | flag.Usage() 129 | os.Exit(1) 130 | } 131 | 132 | if strings.HasPrefix(args[0], "--INTERNAL_FD=") { 133 | fd, err := strconv.Atoi(args[0][len("--INTERNAL_FD="):]) 134 | if err != nil { 135 | log.Fatalf("internal error: Atoi('%s'): %s", args[0], err) 136 | } 137 | if fd < 0 { 138 | log.Fatalf("internal error: expected unsigned int, not %d", fd) 139 | } 140 | args = args[1:] 141 | if len(args) < 1 { 142 | log.Fatalf("internal error: no args") 143 | } 144 | 145 | os.Exit(execInNamespace(fd, args)) 146 | } 147 | 148 | cgroupPath := cgroups.StaticPath(newPath()) 149 | 150 | memLimit := int64(16 * 1024 * 1024 * 1024) 151 | resources := &specs.LinuxResources{ 152 | //CPU: &specs.LinuxCPU{}, 153 | Memory: &specs.LinuxMemory{ 154 | Limit: &memLimit, 155 | Kernel: &memLimit, 156 | }, 157 | } 158 | cgroup, err := cgroups.New(cgroups.V1, cgroupPath, resources) 159 | if err != nil { 160 | log.Fatalf("cgroups.New: %s", err) 161 | } 162 | defer cgroup.Delete() 163 | 164 | // create a pipe to signal to our child when it should 165 | // actually start running (after we stick it into the cgroup) 166 | r, w, err := os.Pipe() 167 | if err != nil { 168 | log.Fatalf("os.Pipe: %s", err) 169 | } 170 | 171 | // use 3 as the file descriptor, as that refers to the first 172 | // FD when using ExtraFiles 173 | internalFlag := fmt.Sprintf("--INTERNAL_FD=%d", 3) // int(r.Fd()) 174 | childArgs := []string{} 175 | for _, envVar := range envVars { 176 | childArgs = append(childArgs, "-env", envVar) 177 | } 178 | childArgs = append(childArgs, "--", internalFlag) 179 | childArgs = append(childArgs, args...) 180 | cmd := exec.Command(os.Args[0], childArgs...) 181 | cmd.ExtraFiles = []*os.File{r} 182 | cmd.Stdin = os.Stdin 183 | cmd.Stdout = os.Stdout 184 | cmd.Stderr = os.Stderr 185 | 186 | if err = cmd.Start(); err != nil { 187 | log.Fatalf("Start: %s", err) 188 | } 189 | 190 | r.Close() 191 | r = nil 192 | 193 | if err := cgroup.Add(cgroups.Process{Pid: cmd.Process.Pid}); err != nil { 194 | log.Fatalf("cg.Add: %s", err) 195 | } 196 | 197 | // give the OS a chance to exec our child and have it waiting 198 | // at read(pipe) 199 | time.Sleep(10 * time.Millisecond) 200 | 201 | poller, err := NewPoller(cgroup, *frequency) 202 | if err != nil { 203 | log.Fatalf("NewPoller: %s", err) 204 | } 205 | 206 | if n, err := w.Write([]byte("ok")); n != 2 || err != nil { 207 | log.Fatalf("pipe.Write: %d/%s", n, err) 208 | } 209 | w.Close() 210 | w = nil 211 | 212 | err = cmd.Wait() 213 | if err != nil { 214 | exitError := err.(*exec.ExitError) 215 | log.Printf("error: %#v", exitError) 216 | os.Exit(1) 217 | } 218 | 219 | runtime.LockOSThread() 220 | defer runtime.UnlockOSThread() 221 | 222 | gid := syscall.Getgid() 223 | if err := syscall.Setresgid(gid, gid, gid); err != nil { 224 | log.Fatalf("Setresgid(%d): %s", gid, err) 225 | } 226 | 227 | uid := syscall.Getuid() 228 | if err := syscall.Setresuid(uid, uid, uid); err != nil { 229 | log.Fatalf("Setresuid(%d): %s", uid, err) 230 | } 231 | 232 | stats := poller.End() 233 | // - (check cgroup is empty?) 234 | // - report on total memory usage 235 | 236 | var buf bytes.Buffer 237 | bio := bufio.NewWriter(&buf) 238 | if _, err := bio.WriteString("time\trss\tkernel\n"); err != nil { 239 | log.Fatalf("bio.WriteString: %s", err) 240 | } 241 | 242 | start := stats.Rss[0].Time.UnixNano() 243 | for i := 0; i < len(stats.Rss); i++ { 244 | r := stats.Rss[i] 245 | line := fmt.Sprintf("%d\t%d\t%d\n", r.Time.UnixNano()-start, r.Value, r.Kernel) 246 | if _, err := bio.WriteString(line); err != nil { 247 | log.Fatalf("bufio.WriteString: %s", err) 248 | } 249 | } 250 | if err = bio.Flush(); err != nil { 251 | log.Fatalf("bio.Flush: %s", err) 252 | } 253 | 254 | if *outputPath != "" { 255 | if err = ioutil.WriteFile(*outputPath, buf.Bytes(), 0666); err != nil { 256 | log.Fatalf("writing output to '%s' failed: %s", *outputPath, err) 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /poller.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Bobby Powers. All rights reserved. 2 | // Use of this source code is governed by the ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "time" 11 | 12 | "github.com/containerd/cgroups" 13 | ) 14 | 15 | type Record struct { 16 | Time time.Time 17 | Value uint64 18 | Kernel uint64 19 | } 20 | 21 | type Stats struct { 22 | Rss []Record 23 | } 24 | 25 | type endReq struct { 26 | result chan<- *Stats 27 | } 28 | 29 | type Poller struct { 30 | in chan<- *endReq 31 | stats *Stats 32 | } 33 | 34 | func NewPoller(cgroup cgroups.Cgroup, freq int) (*Poller, error) { 35 | durationStr := fmt.Sprintf("%fs", 1/float64(freq)) 36 | duration, err := time.ParseDuration(durationStr) 37 | if err != nil { 38 | return nil, fmt.Errorf("bad frequency (%d): %s", freq, err) 39 | } 40 | if duration <= 0 { 41 | return nil, fmt.Errorf("expected positive duration, not %s", duration) 42 | } 43 | 44 | ch := make(chan *endReq) 45 | p := &Poller{ 46 | in: ch, 47 | stats: &Stats{}, 48 | } 49 | 50 | // kick this off once at the start 51 | if err := p.poll(time.Now(), cgroup); err != nil { 52 | return nil, fmt.Errorf("poll: %s", err) 53 | } 54 | 55 | go p.poller(cgroup, ch, duration) 56 | 57 | return p, nil 58 | } 59 | 60 | func (p *Poller) poll(t time.Time, cgroup cgroups.Cgroup) error { 61 | 62 | stats, err := cgroup.Stat(cgroups.IgnoreNotExist) 63 | if err != nil || stats == nil { 64 | return fmt.Errorf("cg.Stat: %s", err) 65 | } 66 | if stats.Memory == nil { 67 | return fmt.Errorf("cg.Stat: returned nil Memory stats") 68 | } 69 | 70 | p.stats.Rss = append(p.stats.Rss, Record{t, stats.Memory.Usage.Usage, stats.Memory.Kernel.Usage}) 71 | 72 | return nil 73 | } 74 | 75 | // loop that runs in its own goroutine, reading stats at the desired 76 | // frequency until shouldEnd is received 77 | func (p *Poller) poller(cgroup cgroups.Cgroup, shouldEnd <-chan *endReq, duration time.Duration) { 78 | 79 | ticker := time.NewTicker(duration) 80 | defer ticker.Stop() 81 | 82 | for { 83 | select { 84 | case waiter := <-shouldEnd: 85 | waiter.result <- p.stats 86 | return 87 | case t := <-ticker.C: 88 | if err := p.poll(t, cgroup); err != nil { 89 | log.Printf("mstat: %s", err) 90 | } 91 | } 92 | } 93 | } 94 | 95 | func (p *Poller) End() *Stats { 96 | result := make(chan *Stats) 97 | p.in <- &endReq{result} 98 | return <-result 99 | } 100 | --------------------------------------------------------------------------------