22 #include <sys/types.h>
27 // The above block of magic allows us to look up user_hz via _SC_CLK_TCK.
35 func CopyPipeToChan(in io.Reader, out chan string, done chan<- bool) {
36 s := bufio.NewScanner(in)
43 func CopyChanToPipe(in <-chan string, out io.Writer) {
49 func OpenAndReadAll(filename string, log_chan chan<- string) ([]byte, error) {
50 in, err := os.Open(filename)
53 log_chan <- fmt.Sprintf("crunchstat: open %s: %s", filename, err)
59 content, err := ioutil.ReadAll(in)
60 if err != nil && log_chan != nil {
61 log_chan <- fmt.Sprintf("crunchstat: read %s: %s", filename, err)
67 var reportedStatFile map[string]bool
68 var reportedNoStatFile map[string]bool
70 // Find the cgroup stats file in /sys/fs corresponding to the target
73 // TODO: Instead of trying all options, choose a process in the
74 // container, and read /proc/PID/cgroup to determine the appropriate
75 // cgroup root for the given statgroup. (This will avoid falling back
76 // to host-level stats during container setup and teardown.)
77 func FindStat(stderr chan<- string, cgroup Cgroup, statgroup string, stat string) string {
78 if reportedStatFile == nil {
79 reportedStatFile = make(map[string]bool)
80 reportedNoStatFile = make(map[string]bool)
84 path = fmt.Sprintf("%s/%s/%s/%s/%s", cgroup.root, statgroup, cgroup.parent, cgroup.cid, stat)
85 if _, err := os.Stat(path); err != nil {
86 path = fmt.Sprintf("%s/%s/%s/%s", cgroup.root, cgroup.parent, cgroup.cid, stat)
88 if _, err := os.Stat(path); err != nil {
89 path = fmt.Sprintf("%s/%s/%s", cgroup.root, statgroup, stat)
91 if _, err := os.Stat(path); err != nil {
92 path = fmt.Sprintf("%s/%s", cgroup.root, stat)
94 if _, err := os.Stat(path); err != nil {
95 if _, ok := reportedNoStatFile[stat]; !ok {
96 stderr <- fmt.Sprintf("crunchstat: did not find stats file (root %s, parent %s, cid %s, statgroup %s, stat %s)", cgroup.root, cgroup.parent, cgroup.cid, statgroup, stat)
97 reportedNoStatFile[stat] = true
101 if _, ok := reportedStatFile[path]; !ok {
102 stderr <- fmt.Sprintf("crunchstat: reading stats from %s", path)
103 reportedStatFile[path] = true
108 func GetContainerNetStats(stderr chan<- string, cgroup Cgroup) (io.Reader, error) {
109 procsFilename := FindStat(stderr, cgroup, "cpuacct", "cgroup.procs")
110 procsFile, err := os.Open(procsFilename)
112 stderr <- fmt.Sprintf("crunchstat: open %s: %s", procsFilename, err)
115 defer procsFile.Close()
116 reader := bufio.NewScanner(procsFile)
118 taskPid := reader.Text()
119 statsFilename := fmt.Sprintf("/proc/%s/net/dev", taskPid)
120 stats, err := OpenAndReadAll(statsFilename, stderr)
124 return strings.NewReader(string(stats)), nil
126 return nil, errors.New("Could not read stats for any proc in container")
129 type IoSample struct {
135 func DoBlkIoStats(stderr chan<- string, cgroup Cgroup, lastSample map[string]IoSample) (map[string]IoSample) {
136 blkio_io_service_bytes := FindStat(stderr, cgroup, "blkio", "blkio.io_service_bytes")
137 if blkio_io_service_bytes == "" {
141 c, err := os.Open(blkio_io_service_bytes)
143 stderr <- fmt.Sprintf("crunchstat: open %s: %s", blkio_io_service_bytes, err)
147 b := bufio.NewScanner(c)
148 var sampleTime = time.Now()
149 newSamples := make(map[string]IoSample)
151 var device, op string
153 if _, err := fmt.Sscanf(string(b.Text()), "%s %s %d", &device, &op, &val); err != nil {
156 var thisSample IoSample
158 if thisSample, ok = newSamples[device]; !ok {
159 thisSample = IoSample{sampleTime, -1, -1}
163 thisSample.rxBytes = val
165 thisSample.txBytes = val
167 newSamples[device] = thisSample
169 if lastSample == nil {
170 lastSample = make(map[string]IoSample)
172 for dev, sample := range newSamples {
173 if sample.txBytes < 0 || sample.rxBytes < 0 {
177 if prev, ok := lastSample[dev]; ok {
178 delta = fmt.Sprintf(" -- interval %.4f seconds %d write %d read",
179 sample.sampleTime.Sub(prev.sampleTime).Seconds(),
180 sample.txBytes - prev.txBytes,
181 sample.rxBytes - prev.rxBytes)
183 stderr <- fmt.Sprintf("crunchstat: blkio:%s %d write %d read%s", dev, sample.txBytes, sample.rxBytes, delta)
184 lastSample[dev] = sample
189 type MemSample struct {
191 memStat map[string]int64
194 func DoMemoryStats(stderr chan<- string, cgroup Cgroup) {
195 memory_stat := FindStat(stderr, cgroup, "memory", "memory.stat")
196 if memory_stat == "" {
199 c, err := os.Open(memory_stat)
201 stderr <- fmt.Sprintf("crunchstat: open %s: %s", memory_stat, err)
205 b := bufio.NewScanner(c)
206 thisSample := MemSample{time.Now(), make(map[string]int64)}
207 wantStats := [...]string{"cache", "pgmajfault", "rss"}
211 if _, err := fmt.Sscanf(string(b.Text()), "%s %d", &stat, &val); err != nil {
214 thisSample.memStat[stat] = val
216 var outstat bytes.Buffer
217 for _, key := range wantStats {
218 if val, ok := thisSample.memStat[key]; ok {
219 outstat.WriteString(fmt.Sprintf(" %d %s", val, key))
222 stderr <- fmt.Sprintf("crunchstat: mem%s", outstat.String())
225 func DoNetworkStats(stderr chan<- string, cgroup Cgroup, lastSample map[string]IoSample) (map[string]IoSample) {
226 sampleTime := time.Now()
227 stats, err := GetContainerNetStats(stderr, cgroup)
228 if err != nil { return lastSample }
230 if lastSample == nil {
231 lastSample = make(map[string]IoSample)
233 scanner := bufio.NewScanner(stats)
234 Iface: for scanner.Scan() {
237 words := bufio.NewScanner(strings.NewReader(scanner.Text()))
238 words.Split(bufio.ScanWords)
244 ifName = strings.TrimRight(word, ":")
246 if _, err := fmt.Sscanf(word, "%d", &rx); err != nil {
250 if _, err := fmt.Sscanf(word, "%d", &tx); err != nil {
256 if ifName == "lo" || ifName == "" || wordIndex != 17 {
257 // Skip loopback interface and lines with wrong format
260 nextSample := IoSample{}
261 nextSample.sampleTime = sampleTime
262 nextSample.txBytes = tx
263 nextSample.rxBytes = rx
265 if lastSample, ok := lastSample[ifName]; ok {
266 interval := nextSample.sampleTime.Sub(lastSample.sampleTime).Seconds()
267 delta = fmt.Sprintf(" -- interval %.4f seconds %d tx %d rx",
269 tx - lastSample.txBytes,
270 rx - lastSample.rxBytes)
272 stderr <- fmt.Sprintf("crunchstat: net:%s %d tx %d rx%s",
273 ifName, tx, rx, delta)
274 lastSample[ifName] = nextSample
279 type CpuSample struct {
286 func DoCpuStats(stderr chan<- string, cgroup Cgroup, cpus int64, user_hz float64, lastSample *CpuSample) (*CpuSample) {
287 cpuacct_stat := FindStat(stderr, cgroup, "cpuacct", "cpuacct.stat")
288 if cpuacct_stat == "" {
291 b, err := OpenAndReadAll(cpuacct_stat, stderr)
295 nextSample := &CpuSample{time.Now(), 0, 0, cpus}
296 var userTicks, sysTicks int64
297 fmt.Sscanf(string(b), "user %d\nsystem %d", &nextSample.user, &nextSample.sys)
298 nextSample.user = float64(userTicks) / user_hz
299 nextSample.sys = float64(sysTicks) / user_hz
302 if lastSample != nil {
303 delta = fmt.Sprintf(" -- interval %.4f seconds %.4f user %.4f sys",
304 nextSample.sampleTime.Sub(lastSample.sampleTime).Seconds(),
305 nextSample.user - lastSample.user,
306 nextSample.sys - lastSample.sys)
308 stderr <- fmt.Sprintf("crunchstat: cpu %.4f user %.4f sys %d cpus%s",
309 nextSample.user, nextSample.sys, cpus, delta)
313 func PollCgroupStats(cgroup Cgroup, stderr chan string, poll int64, stop_poll_chan <-chan bool) {
314 var last_cpucount int64 = 0
316 user_hz := float64(C.sysconf(C._SC_CLK_TCK))
318 var lastNetSample map[string]IoSample = nil
319 var lastDiskSample map[string]IoSample = nil
320 var lastCpuSample *CpuSample = nil
322 poll_chan := make(chan bool, 1)
324 // Send periodic poll events.
327 time.Sleep(time.Duration(poll) * time.Millisecond)
333 case <-stop_poll_chan:
336 // Emit stats, then select again.
338 cpuset_cpus := FindStat(stderr, cgroup, "cpuset", "cpuset.cpus")
339 if cpuset_cpus != "" {
340 b, err := OpenAndReadAll(cpuset_cpus, stderr)
342 // cgroup probably gone -- skip other stats too.
345 sp := strings.Split(string(b), ",")
347 for _, v := range sp {
349 n, _ := fmt.Sscanf(v, "%d-%d", &min, &max)
351 cpus += (max - min) + 1
359 DoMemoryStats(stderr, cgroup)
360 lastCpuSample = DoCpuStats(stderr, cgroup, last_cpucount, user_hz, lastCpuSample)
361 lastDiskSample = DoBlkIoStats(stderr, cgroup, lastDiskSample)
362 lastNetSample = DoNetworkStats(stderr, cgroup, lastNetSample)
366 func run(logger *log.Logger) error {
371 cgroup_cidfile string
376 flag.StringVar(&cgroup_root, "cgroup-root", "", "Root of cgroup tree")
377 flag.StringVar(&cgroup_parent, "cgroup-parent", "", "Name of container parent under cgroup")
378 flag.StringVar(&cgroup_cidfile, "cgroup-cid", "", "Path to container id file")
379 flag.Int64Var(&wait, "wait", 5, "Maximum time (in seconds) to wait for cid file to show up")
380 flag.Int64Var(&poll, "poll", 1000, "Polling frequency, in milliseconds")
384 if cgroup_root == "" {
385 logger.Fatal("Must provide -cgroup-root")
388 stderr_chan := make(chan string, 1)
389 defer close(stderr_chan)
390 finish_chan := make(chan bool)
391 defer close(finish_chan)
393 go CopyChanToPipe(stderr_chan, os.Stderr)
397 if len(flag.Args()) > 0 {
399 cmd = exec.Command(flag.Args()[0], flag.Args()[1:]...)
401 logger.Print("Running ", flag.Args())
403 // Child process will use our stdin and stdout pipes
404 // (we close our copies below)
406 cmd.Stdout = os.Stdout
408 // Forward SIGINT and SIGTERM to inner process
409 term := make(chan os.Signal, 1)
410 go func(sig <-chan os.Signal) {
412 if cmd.Process != nil {
413 cmd.Process.Signal(catch)
415 logger.Print("caught signal: ", catch)
417 signal.Notify(term, syscall.SIGTERM)
418 signal.Notify(term, syscall.SIGINT)
420 // Funnel stderr through our channel
421 stderr_pipe, err := cmd.StderrPipe()
425 go CopyPipeToChan(stderr_pipe, stderr_chan, finish_chan)
428 if err := cmd.Start(); err != nil {
432 // Close stdin/stdout in this (parent) process
438 var container_id string
439 if cgroup_cidfile != "" {
440 // wait up to 'wait' seconds for the cid file to appear
443 for i = 0; i < time.Duration(wait)*time.Second; i += (100 * time.Millisecond) {
444 cid, err := OpenAndReadAll(cgroup_cidfile, nil)
445 if err == nil && len(cid) > 0 {
447 container_id = string(cid)
450 time.Sleep(100 * time.Millisecond)
453 logger.Printf("Could not read cid file %s", cgroup_cidfile)
457 stop_poll_chan := make(chan bool, 1)
458 cgroup := Cgroup{cgroup_root, cgroup_parent, container_id}
459 go PollCgroupStats(cgroup, stderr_chan, poll, stop_poll_chan)
461 // When the child exits, tell the polling goroutine to stop.
462 defer func() { stop_poll_chan <- true }()
464 // Wait for CopyPipeToChan to consume child's stderr pipe
471 logger := log.New(os.Stderr, "crunchstat: ", 0)
472 if err := run(logger); err != nil {
473 if exiterr, ok := err.(*exec.ExitError); ok {
474 // The program has exited with an exit code != 0
476 // This works on both Unix and
477 // Windows. Although package syscall is
478 // generally platform dependent, WaitStatus is
479 // defined for both Unix and Windows and in
480 // both cases has an ExitStatus() method with
481 // the same signature.
482 if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
483 os.Exit(status.ExitStatus())
486 logger.Fatalf("cmd.Wait: %v", err)