├── .travis.yml ├── stat.go ├── cmd └── ustat │ ├── main.go │ ├── record.go │ └── report.go ├── LICENSE ├── CHANGELOG.md ├── README.md ├── softirqs.go ├── interrupts.go ├── disk.go ├── net.go └── cpus.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | notifications: 4 | email: false 5 | go: 6 | - 1.6.x 7 | - 1.7.x 8 | - 1.8.x 9 | - 1.9.x 10 | - master 11 | before_script: 12 | - go get gopkg.in/urfave/cli.v1 13 | - go get github.com/c9s/goprocinfo/linux 14 | - go get github.com/montanaflynn/stats 15 | script: 16 | - go test -v ./... 17 | -------------------------------------------------------------------------------- /stat.go: -------------------------------------------------------------------------------- 1 | package ustat 2 | 3 | // A Stat is a collection of named stats. 4 | type Stat struct { 5 | Names []string 6 | Descriptions []string 7 | Collector StatCollector 8 | } 9 | 10 | // A StatCollector is an interface for collecting stats. 11 | type StatCollector interface { 12 | Collect() []uint64 13 | } 14 | 15 | // Difference calculates the change in values for two arrays. 16 | func Difference(before []uint64, after []uint64) []uint64 { 17 | var diff []uint64 18 | for idx := range before { 19 | diff = append(diff, after[idx]-before[idx]) 20 | } 21 | return diff 22 | } 23 | -------------------------------------------------------------------------------- /cmd/ustat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/urfave/cli.v1" 6 | "os" 7 | ) 8 | 9 | const version = "0.2.0" 10 | 11 | func main() { 12 | app := cli.NewApp() 13 | app.Name = "ustat" 14 | app.Version = version 15 | app.Usage = "Unified system statistics collector" 16 | app.Authors = []cli.Author{ 17 | cli.Author{ 18 | Name: "Pekka Enberg", 19 | Email: "penberg@iki.fi", 20 | }, 21 | } 22 | app.HideHelp = true 23 | app.Commands = []cli.Command{ 24 | recordCommand, 25 | reportCommand, 26 | } 27 | if err := app.Run(os.Args); err != nil { 28 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 29 | os.Exit(1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pekka Enberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.2.0] - 2017-07-13 10 | ### Added 11 | - Collect aggregate CPU stats. 12 | - Collect SoftIRQ stats. 13 | - Add `ustat report` command for summarizing results. 14 | 15 | ### Changed 16 | - Move stats collection under `ustat record` command. 17 | 18 | ### Fixed 19 | - Fix CPU utilization percentage calculation. 20 | 21 | ## 0.1.0 - 2017-06-08 22 | ### Added 23 | - A `ustat` command line tool written in Go, similar to `dstat`, for example. 24 | - CPU utilization stats per core and number of context switches, which are collected from `/proc/stat`. 25 | - Interrupt count stats per core, which are collected from `/proc/interrupts`. 26 | - Network stats per interface, which are collected from `/proc/net/dev`. 27 | - Disk stats per block device, which are collected from `/proc/diskstats`. 28 | 29 | [Unreleased]: https://github.com/penberg/ustat/compare/v0.2.0...HEAD 30 | [0.2.0]: https://github.com/penberg/ustat/compare/v0.1.0...v0.2.0 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ustat 2 | 3 | [![Build Status](https://travis-ci.org/penberg/ustat.svg?branch=master)](http://travis-ci.org/penberg/ustat) 4 | 5 | `ustat` is an unified system stats collector for Linux, which combines capabilities of tools like `vmstat`, `mpstat`, `iostat`, and `ifstat`. 6 | The tool is designed for low collection overhead to make it suitable for stats collection when evaluating system performance under load. 7 | The main objective of `ustat` is to collect detailed stats rather than aggregate stats so that it is possible to drill down to details during analysis. 8 | The `ustat` tool reports collected stats in a self-describing, [delimiter-separated values](https://en.wikipedia.org/wiki/Delimiter-separated_values) (DSV) format file that is easy to post process using tools like [ggplot2](http://ggplot2.org/) for R and [gnuplot](http://www.gnuplot.info/). 9 | 10 | ## Install 11 | 12 | ```sh 13 | go get -u github.com/penberg/ustat/cmd/ustat 14 | ``` 15 | 16 | ## Usage 17 | 18 | To collect stats, run: 19 | 20 | ```sh 21 | ustat record 1 22 | ``` 23 | 24 | In the above example, `ustat` collects all stats it supports and samples them every one second. 25 | 26 | Please use the `ustat --help` command for more information on supported stats collectors and other command line options. 27 | 28 | ## Related Tools 29 | 30 | * [dstat](http://dag.wiee.rs/home-made/dstat/) - Versatile resource statistics tool. The tool provides similar capabilities as `ustat` but is written in [Python](https://www.python.org/), which has higher collection overhead, and does not provide detailed stats for everything (e.g. interrupts). 31 | 32 | ## Authors 33 | 34 | * [Pekka Enberg](https://penberg.github.io/) 35 | 36 | See also the list of [contributors](https://github.com/penberg/ustat/contributors) who participated in this project. 37 | 38 | ## License 39 | 40 | `ustat` is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 41 | -------------------------------------------------------------------------------- /softirqs.go: -------------------------------------------------------------------------------- 1 | package ustat 2 | 3 | import ( 4 | "fmt" 5 | procfs "github.com/c9s/goprocinfo/linux" 6 | ) 7 | 8 | type procSoftIRQsCollector struct { 9 | counts []uint64 10 | } 11 | 12 | const procSoftIRQsPath = "/proc/softirqs" 13 | 14 | // NewSoftIRQsStat returns a new Stat, which collects interrupt stats from /proc/interrupts. 15 | func NewSoftIRQsStat() *Stat { 16 | interrupts, err := procfs.ReadInterrupts(procSoftIRQsPath) 17 | if err != nil { 18 | panic(err) 19 | } 20 | names := parseSoftIRQNames(interrupts) 21 | descriptions := parseSoftIRQDescriptions(interrupts) 22 | counts := parseSoftIRQCounts(interrupts) 23 | return &Stat{ 24 | Names: names, 25 | Descriptions: descriptions, 26 | Collector: &procSoftIRQsCollector{counts: counts}, 27 | } 28 | } 29 | 30 | func (reader *procSoftIRQsCollector) Collect() []uint64 { 31 | interrupts, err := procfs.ReadInterrupts(procSoftIRQsPath) 32 | if err != nil { 33 | panic(err) 34 | } 35 | counts := parseSoftIRQCounts(interrupts) 36 | diff := Difference(reader.counts, counts) 37 | reader.counts = counts 38 | return diff 39 | } 40 | 41 | func parseSoftIRQNames(interrupts *procfs.Interrupts) []string { 42 | var names []string 43 | for _, interrupt := range interrupts.Interrupts { 44 | for cpu := range interrupt.Counts { 45 | name := fmt.Sprintf("softirq.%s.cpu%d", interrupt.Name, cpu) 46 | names = append(names, name) 47 | } 48 | } 49 | return names 50 | } 51 | 52 | func parseSoftIRQDescriptions(interrupts *procfs.Interrupts) []string { 53 | var descriptions []string 54 | for _, interrupt := range interrupts.Interrupts { 55 | description := fmt.Sprintf("softirq.%s = %s", interrupt.Name, interrupt.Description) 56 | descriptions = append(descriptions, description) 57 | } 58 | return descriptions 59 | } 60 | 61 | func parseSoftIRQCounts(interrupts *procfs.Interrupts) []uint64 { 62 | var values []uint64 63 | for _, interrupt := range interrupts.Interrupts { 64 | for _, count := range interrupt.Counts { 65 | values = append(values, count) 66 | } 67 | } 68 | return values 69 | } 70 | -------------------------------------------------------------------------------- /interrupts.go: -------------------------------------------------------------------------------- 1 | package ustat 2 | 3 | import ( 4 | "fmt" 5 | procfs "github.com/c9s/goprocinfo/linux" 6 | ) 7 | 8 | type procInterruptsCollector struct { 9 | counts []uint64 10 | } 11 | 12 | const procInterruptsPath = "/proc/interrupts" 13 | 14 | // NewInterruptsStat returns a new Stat, which collects interrupt stats from /proc/interrupts. 15 | func NewInterruptsStat() *Stat { 16 | interrupts, err := procfs.ReadInterrupts(procInterruptsPath) 17 | if err != nil { 18 | panic(err) 19 | } 20 | names := parseInterruptNames(interrupts) 21 | descriptions := parseInterruptDescriptions(interrupts) 22 | counts := parseInterruptCounts(interrupts) 23 | return &Stat{ 24 | Names: names, 25 | Descriptions: descriptions, 26 | Collector: &procInterruptsCollector{counts: counts}, 27 | } 28 | } 29 | 30 | func (reader *procInterruptsCollector) Collect() []uint64 { 31 | interrupts, err := procfs.ReadInterrupts(procInterruptsPath) 32 | if err != nil { 33 | panic(err) 34 | } 35 | counts := parseInterruptCounts(interrupts) 36 | diff := Difference(reader.counts, counts) 37 | reader.counts = counts 38 | return diff 39 | } 40 | 41 | func parseInterruptNames(interrupts *procfs.Interrupts) []string { 42 | var names []string 43 | for _, interrupt := range interrupts.Interrupts { 44 | for cpu := range interrupt.Counts { 45 | name := fmt.Sprintf("int%s.cpu%d", interrupt.Name, cpu) 46 | names = append(names, name) 47 | } 48 | } 49 | return names 50 | } 51 | 52 | func parseInterruptDescriptions(interrupts *procfs.Interrupts) []string { 53 | var descriptions []string 54 | for _, interrupt := range interrupts.Interrupts { 55 | description := fmt.Sprintf("intr.%s = %s", interrupt.Name, interrupt.Description) 56 | descriptions = append(descriptions, description) 57 | } 58 | return descriptions 59 | } 60 | 61 | func parseInterruptCounts(interrupts *procfs.Interrupts) []uint64 { 62 | var values []uint64 63 | for _, interrupt := range interrupts.Interrupts { 64 | for _, count := range interrupt.Counts { 65 | values = append(values, count) 66 | } 67 | } 68 | return values 69 | } 70 | -------------------------------------------------------------------------------- /disk.go: -------------------------------------------------------------------------------- 1 | package ustat 2 | 3 | import ( 4 | "fmt" 5 | procfs "github.com/c9s/goprocinfo/linux" 6 | ) 7 | 8 | type diskStatCollector struct { 9 | values []uint64 10 | } 11 | 12 | const procDiskStatPath = "/proc/diskstats" 13 | 14 | // NewDiskStat returns a new Stat, which collects disk stats from /proc/diskstats. 15 | func NewDiskStat() *Stat { 16 | stats, err := procfs.ReadDiskStats(procDiskStatPath) 17 | if err != nil { 18 | panic(err) 19 | } 20 | names := parseDiskStatNames(stats) 21 | descriptions := parseDiskStatDescriptions(stats) 22 | values := parseDiskStats(stats) 23 | return &Stat{ 24 | Names: names, 25 | Descriptions: descriptions, 26 | Collector: &diskStatCollector{values: values}, 27 | } 28 | } 29 | 30 | func (reader *diskStatCollector) Collect() []uint64 { 31 | stats, err := procfs.ReadDiskStats(procDiskStatPath) 32 | if err != nil { 33 | panic(err) 34 | } 35 | values := parseDiskStats(stats) 36 | diff := Difference(reader.values, values) 37 | reader.values = values 38 | return diff 39 | } 40 | 41 | var diskStatTypes = []string{ 42 | "read.sectors", 43 | "write.sectors", 44 | } 45 | 46 | var diskStatDescriptions = map[string]string{ 47 | "read.sectors": "Number of 512 byte sectors read", 48 | "write.sectors": "Number of 512 byte sectors written", 49 | } 50 | 51 | func parseDiskStatNames(stats []procfs.DiskStat) []string { 52 | var names []string 53 | for _, stat := range stats { 54 | for _, diskStatType := range diskStatTypes { 55 | name := fmt.Sprintf("disk.%s.%s", stat.Name, diskStatType) 56 | names = append(names, name) 57 | } 58 | } 59 | return names 60 | } 61 | 62 | func parseDiskStatDescriptions(stats []procfs.DiskStat) []string { 63 | var descriptions []string 64 | for _, stat := range stats { 65 | for _, diskStatType := range diskStatTypes { 66 | diskStatDescription := diskStatDescriptions[diskStatType] 67 | description := fmt.Sprintf("disk.%s.%s = %s %s", stat.Name, diskStatType, stat.Name, diskStatDescription) 68 | descriptions = append(descriptions, description) 69 | } 70 | } 71 | return descriptions 72 | } 73 | 74 | func parseDiskStats(stats []procfs.DiskStat) []uint64 { 75 | var values []uint64 76 | for _, stat := range stats { 77 | values = append(values, stat.ReadSectors) 78 | values = append(values, stat.WriteSectors) 79 | } 80 | return values 81 | } 82 | -------------------------------------------------------------------------------- /net.go: -------------------------------------------------------------------------------- 1 | package ustat 2 | 3 | import ( 4 | "fmt" 5 | procfs "github.com/c9s/goprocinfo/linux" 6 | ) 7 | 8 | type procNetDevCollector struct { 9 | values []uint64 10 | } 11 | 12 | const procNetDevPath = "/proc/net/dev" 13 | 14 | // NewNetStat returns a new Stat, which collects networking stats from /proc/net/dev. 15 | func NewNetStat() *Stat { 16 | stats, err := procfs.ReadNetworkStat(procNetDevPath) 17 | if err != nil { 18 | panic(err) 19 | } 20 | names := parseNetStatNames(stats) 21 | descriptions := parseNetStatDescriptions(stats) 22 | values := parseNetStats(stats) 23 | return &Stat{ 24 | Names: names, 25 | Descriptions: descriptions, 26 | Collector: &procNetDevCollector{values: values}, 27 | } 28 | } 29 | 30 | func (reader *procNetDevCollector) Collect() []uint64 { 31 | stats, err := procfs.ReadNetworkStat(procNetDevPath) 32 | if err != nil { 33 | panic(err) 34 | } 35 | values := parseNetStats(stats) 36 | diff := Difference(reader.values, values) 37 | reader.values = values 38 | return diff 39 | } 40 | 41 | var netStatTypes = []string{ 42 | "rx.bytes", 43 | "rx.packets", 44 | "rx.errors", 45 | "rx.drop", 46 | "tx.bytes", 47 | "tx.packets", 48 | "tx.errors", 49 | "tx.drop", 50 | } 51 | 52 | var netStatDescriptions = map[string]string{ 53 | "rx.bytes": "Number of bytes received", 54 | "rx.packets": "Number of packets received", 55 | "rx.errors": "Number of receive errors", 56 | "rx.drop": "Number of receive packets dropped", 57 | "tx.bytes": "Number of bytes transmitd", 58 | "tx.packets": "Number of packets transmitd", 59 | "tx.errors": "Number of transmit errors", 60 | "tx.drop": "Number of transmit packets dropped", 61 | } 62 | 63 | func parseNetStatNames(stats []procfs.NetworkStat) []string { 64 | var names []string 65 | for _, stat := range stats { 66 | for _, netStatType := range netStatTypes { 67 | name := fmt.Sprintf("net.%s.%s", stat.Iface, netStatType) 68 | names = append(names, name) 69 | } 70 | } 71 | return names 72 | } 73 | 74 | func parseNetStatDescriptions(stats []procfs.NetworkStat) []string { 75 | var descriptions []string 76 | for _, stat := range stats { 77 | for _, netStatType := range netStatTypes { 78 | netStatDescription := netStatDescriptions[netStatType] 79 | description := fmt.Sprintf("net.%s.%s = %s %s", stat.Iface, netStatType, stat.Iface, netStatDescription) 80 | descriptions = append(descriptions, description) 81 | } 82 | } 83 | return descriptions 84 | } 85 | 86 | func parseNetStats(stats []procfs.NetworkStat) []uint64 { 87 | var values []uint64 88 | for _, stat := range stats { 89 | values = append(values, stat.RxBytes) 90 | values = append(values, stat.RxPackets) 91 | values = append(values, stat.RxErrs) 92 | values = append(values, stat.RxDrop) 93 | values = append(values, stat.TxBytes) 94 | values = append(values, stat.TxPackets) 95 | values = append(values, stat.TxErrs) 96 | values = append(values, stat.TxDrop) 97 | } 98 | return values 99 | } 100 | -------------------------------------------------------------------------------- /cmd/ustat/record.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/penberg/ustat" 6 | "gopkg.in/urfave/cli.v1" 7 | "os" 8 | "os/signal" 9 | "regexp" 10 | "strconv" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | const defaultDelay = 1 16 | 17 | var recordCommand = cli.Command{ 18 | Name: "record", 19 | Usage: "record system stats", 20 | ArgsUsage: "[delay]", 21 | Flags: []cli.Flag{ 22 | cli.BoolFlag{ 23 | Name: "c,cpu", 24 | Usage: "enable CPU stats collection", 25 | }, 26 | cli.BoolFlag{ 27 | Name: "i,int", 28 | Usage: "enable interrupt stats collection", 29 | }, 30 | cli.BoolFlag{ 31 | Name: "s,softirq", 32 | Usage: "enable softirq stats collection", 33 | }, 34 | cli.BoolFlag{ 35 | Name: "n,net", 36 | Usage: "enable network stats collection", 37 | }, 38 | cli.BoolFlag{ 39 | Name: "d,disk", 40 | Usage: "enable disk stats collection", 41 | }, 42 | cli.StringFlag{ 43 | Name: "o,output", 44 | Usage: "write output to `FILE`", 45 | }, 46 | cli.StringFlag{ 47 | Name: "delimiter", 48 | Usage: "delimiter used in the output file", 49 | Value: "\t", 50 | }, 51 | cli.StringFlag{ 52 | Name: "grep", 53 | Usage: "filter stats using an regular expression `PATTERN`", 54 | }, 55 | }, 56 | Action: recordAction, 57 | } 58 | 59 | func recordAction(ctx *cli.Context) error { 60 | var stats []*ustat.Stat 61 | if ctx.Bool("cpu") { 62 | stats = append(stats, ustat.NewCPUsStat()) 63 | } 64 | if ctx.Bool("int") { 65 | stats = append(stats, ustat.NewInterruptsStat()) 66 | } 67 | if ctx.Bool("softirq") { 68 | stats = append(stats, ustat.NewSoftIRQsStat()) 69 | } 70 | if ctx.Bool("net") { 71 | stats = append(stats, ustat.NewNetStat()) 72 | } 73 | if ctx.Bool("disk") { 74 | stats = append(stats, ustat.NewDiskStat()) 75 | } 76 | output := os.Stdout 77 | outputPath := ctx.String("output") 78 | if outputPath != "" { 79 | file, err := os.Create(outputPath) 80 | if err != nil { 81 | return cli.NewExitError(fmt.Sprintf("Unable to open file: %v", err), 2) 82 | } 83 | defer file.Close() 84 | output = file 85 | } 86 | pattern := ctx.String("grep") 87 | if len(stats) == 0 { 88 | stats = []*ustat.Stat{ 89 | ustat.NewCPUsStat(), 90 | ustat.NewInterruptsStat(), 91 | ustat.NewSoftIRQsStat(), 92 | ustat.NewNetStat(), 93 | ustat.NewDiskStat(), 94 | } 95 | } 96 | delimiter := ctx.String("delimiter") 97 | delay := defaultDelay 98 | args := ctx.Args() 99 | if len(args) > 0 { 100 | rawDelay := args[0] 101 | var err error 102 | delay, err = strconv.Atoi(rawDelay) 103 | if err != nil { 104 | return cli.NewExitError(fmt.Sprintf("Failed to parse delay argument: '%s'", rawDelay), 3) 105 | } 106 | 107 | } 108 | fmt.Fprintf(output, "# This file has been generated by ustat.\n") 109 | fmt.Fprintf(output, "#\n") 110 | fmt.Fprintf(output, "# Column descriptions:\n") 111 | for _, stat := range stats { 112 | for _, description := range stat.Descriptions { 113 | if pattern != "" { 114 | matched, err := regexp.MatchString(pattern, description) 115 | if err != nil || !matched { 116 | continue 117 | } 118 | } 119 | fmt.Fprintf(output, "# %s\n", description) 120 | } 121 | } 122 | for statIdx, stat := range stats { 123 | for nameIdx, name := range stat.Names { 124 | if pattern != "" { 125 | matched, err := regexp.MatchString(pattern, name) 126 | if err != nil || !matched { 127 | continue 128 | } 129 | } 130 | if statIdx+nameIdx == 0 { 131 | fmt.Fprintf(output, "%s", name) 132 | } else { 133 | fmt.Fprintf(output, "%s%s", delimiter, name) 134 | } 135 | } 136 | } 137 | fmt.Fprintln(output, "") 138 | sigs := make(chan os.Signal, 1) 139 | done := make(chan bool, 1) 140 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 141 | go func() { 142 | sig := <-sigs 143 | fmt.Println() 144 | fmt.Println(sig) 145 | done <- true 146 | }() 147 | ticker := time.NewTicker(time.Duration(delay) * time.Second) 148 | go func() { 149 | for _ = range ticker.C { 150 | for statIdx, stat := range stats { 151 | values := stat.Collector.Collect() 152 | for valueIdx, value := range values { 153 | if pattern != "" { 154 | matched, err := regexp.MatchString(pattern, stat.Names[valueIdx]) 155 | if err != nil || !matched { 156 | continue 157 | } 158 | } 159 | if statIdx+valueIdx == 0 { 160 | fmt.Fprintf(output, "%d", value) 161 | } else { 162 | fmt.Fprintf(output, "%s%d", delimiter, value) 163 | } 164 | } 165 | } 166 | fmt.Fprintln(output, "") 167 | } 168 | }() 169 | <-done 170 | 171 | return nil 172 | } 173 | -------------------------------------------------------------------------------- /cpus.go: -------------------------------------------------------------------------------- 1 | package ustat 2 | 3 | import ( 4 | "fmt" 5 | procfs "github.com/c9s/goprocinfo/linux" 6 | ) 7 | 8 | type procStatCollector struct { 9 | prev *procfs.Stat 10 | } 11 | 12 | const procStatPath = "/proc/stat" 13 | 14 | // NewCPUsStat returns a new Stat, which collects CPU stats from /proc/stat. 15 | func NewCPUsStat() *Stat { 16 | stat, err := procfs.ReadStat(procStatPath) 17 | if err != nil { 18 | panic(err) 19 | } 20 | names := parseCPUStatNames(stat) 21 | descriptions := parseCPUStatDescriptions(stat) 22 | return &Stat{ 23 | Names: names, 24 | Descriptions: descriptions, 25 | Collector: &procStatCollector{ 26 | prev: stat, 27 | }, 28 | } 29 | return nil 30 | } 31 | 32 | func interval(before []uint64, after []uint64) uint64 { 33 | var prev uint64 = 0 34 | for _, value := range before { 35 | prev += value 36 | } 37 | var curr uint64 = 0 38 | for _, value := range after { 39 | curr += value 40 | } 41 | return curr - prev 42 | } 43 | 44 | func (reader *procStatCollector) Collect() []uint64 { 45 | stat, err := procfs.ReadStat(procStatPath) 46 | if err != nil { 47 | panic(err) 48 | } 49 | values := parseCPUStats(stat, reader.prev) 50 | reader.prev = stat 51 | return values 52 | } 53 | 54 | var cpuStatTypes = []string{ 55 | "usr", 56 | "nice", 57 | "system", 58 | "idle", 59 | "iowait", 60 | "irq", 61 | "softirq", 62 | "steal", 63 | "guest", 64 | "guestnice", 65 | } 66 | 67 | var cpuStatDescriptions = map[string]string{ 68 | "usr": "User", 69 | "nice": "Nice", 70 | "system": "System", 71 | "idle": "Idle", 72 | "iowait": "IOWait", 73 | "irq": "IRQ", 74 | "softirq": "SoftIRQ", 75 | "steal": "Steal", 76 | "guest": "Guest", 77 | "guestnice": "GuestNice", 78 | } 79 | 80 | func parseCPUStatNames(stat *procfs.Stat) []string { 81 | var names []string 82 | for _, cpuStatType := range cpuStatTypes { 83 | name := fmt.Sprintf("%s.%s", "cpu", cpuStatType) 84 | names = append(names, name) 85 | } 86 | for _, cpuStat := range stat.CPUStats { 87 | for _, cpuStatType := range cpuStatTypes { 88 | name := fmt.Sprintf("%s.%s", cpuStat.Id, cpuStatType) 89 | names = append(names, name) 90 | } 91 | } 92 | names = append(names, "ctxt.switch") 93 | return names 94 | } 95 | 96 | func parseCPUStatDescriptions(stat *procfs.Stat) []string { 97 | var descriptions []string 98 | for _, cpuStat := range stat.CPUStats { 99 | for _, cpuStatType := range cpuStatTypes { 100 | cpuStatDescription := cpuStatDescriptions[cpuStatType] 101 | description := fmt.Sprintf("%s.%s = %s %s", cpuStat.Id, cpuStatType, cpuStat.Id, cpuStatDescription) 102 | descriptions = append(descriptions, description) 103 | } 104 | } 105 | descriptions = append(descriptions, "ctx.switch = Number of context switches") 106 | return descriptions 107 | } 108 | 109 | func parseCPUStats(curr *procfs.Stat, prev *procfs.Stat) []uint64 { 110 | var values []uint64 111 | interval := runtime(curr.CPUStatAll) - runtime(prev.CPUStatAll) 112 | values = append(values, difference(curr.CPUStatAll.User, prev.CPUStatAll.User, interval)) 113 | values = append(values, difference(curr.CPUStatAll.Nice, prev.CPUStatAll.Nice, interval)) 114 | values = append(values, difference(curr.CPUStatAll.System, prev.CPUStatAll.System, interval)) 115 | values = append(values, difference(curr.CPUStatAll.Idle, prev.CPUStatAll.Idle, interval)) 116 | values = append(values, difference(curr.CPUStatAll.IOWait, prev.CPUStatAll.IOWait, interval)) 117 | values = append(values, difference(curr.CPUStatAll.IRQ, prev.CPUStatAll.IRQ, interval)) 118 | values = append(values, difference(curr.CPUStatAll.SoftIRQ, prev.CPUStatAll.SoftIRQ, interval)) 119 | values = append(values, difference(curr.CPUStatAll.Steal, prev.CPUStatAll.Steal, interval)) 120 | values = append(values, difference(curr.CPUStatAll.Guest, prev.CPUStatAll.Guest, interval)) 121 | values = append(values, difference(curr.CPUStatAll.GuestNice, prev.CPUStatAll.GuestNice, interval)) 122 | for idx, currCpuStat := range curr.CPUStats { 123 | prevCpuStat := prev.CPUStats[idx] 124 | interval := runtime(currCpuStat) - runtime(prevCpuStat) 125 | values = append(values, difference(currCpuStat.User, prevCpuStat.User, interval)) 126 | values = append(values, difference(currCpuStat.Nice, prevCpuStat.Nice, interval)) 127 | values = append(values, difference(currCpuStat.System, prevCpuStat.System, interval)) 128 | values = append(values, difference(currCpuStat.Idle, prevCpuStat.Idle, interval)) 129 | values = append(values, difference(currCpuStat.IOWait, prevCpuStat.IOWait, interval)) 130 | values = append(values, difference(currCpuStat.IRQ, prevCpuStat.IRQ, interval)) 131 | values = append(values, difference(currCpuStat.SoftIRQ, prevCpuStat.SoftIRQ, interval)) 132 | values = append(values, difference(currCpuStat.Steal, prevCpuStat.Steal, interval)) 133 | values = append(values, difference(currCpuStat.Guest, prevCpuStat.Guest, interval)) 134 | values = append(values, difference(currCpuStat.GuestNice, prevCpuStat.GuestNice, interval)) 135 | } 136 | values = append(values, curr.ContextSwitches-prev.ContextSwitches) 137 | return values 138 | } 139 | 140 | func runtime(cpuStat procfs.CPUStat) uint64 { 141 | return cpuStat.User + cpuStat.Nice + cpuStat.System + cpuStat.Idle + cpuStat.IOWait + cpuStat.IRQ + cpuStat.SoftIRQ 142 | } 143 | 144 | func difference(curr uint64, prev uint64, interval uint64) uint64 { 145 | return uint64(float64(curr-prev) / float64(interval) * 100) 146 | } 147 | -------------------------------------------------------------------------------- /cmd/ustat/report.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "github.com/montanaflynn/stats" 7 | "gopkg.in/urfave/cli.v1" 8 | "io" 9 | "os" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "unicode/utf8" 14 | ) 15 | 16 | type cpuStat struct { 17 | values map[string][]float64 18 | } 19 | 20 | type interruptStat struct { 21 | values map[string][]float64 22 | } 23 | 24 | var reportCommand = cli.Command{ 25 | Name: "report", 26 | Usage: "summarise stats that are recored to a file", 27 | ArgsUsage: "[file]", 28 | Flags: []cli.Flag{ 29 | cli.StringFlag{ 30 | Name: "delimiter", 31 | Usage: "delimiter used in the output file", 32 | Value: "\t", 33 | }, 34 | }, 35 | Action: reportAction, 36 | } 37 | 38 | func reportAction(ctx *cli.Context) error { 39 | args := ctx.Args() 40 | if len(args) == 0 { 41 | return cli.NewExitError(fmt.Sprintf("No stats file specified"), 3) 42 | } 43 | filename := args[0] 44 | delimiter := ctx.String("delimiter") 45 | file, err := os.Open(filename) 46 | if err != nil { 47 | return cli.NewExitError(fmt.Sprintf("Unable to open file: %v", err), 2) 48 | } 49 | defer file.Close() 50 | reader := csv.NewReader(file) 51 | comma, _ := utf8.DecodeRuneInString(delimiter) 52 | reader.Comma = comma 53 | header := map[string]int{} 54 | cpuStats := map[string]cpuStat{} 55 | interruptStats := map[string]interruptStat{} 56 | softIrqStats := map[string]interruptStat{} 57 | sampleCount := 0 58 | for { 59 | record, err := reader.Read() 60 | if err == io.EOF { 61 | break 62 | } 63 | if len(record) == 0 { 64 | continue 65 | } 66 | if strings.HasPrefix(record[0], "#") { 67 | continue 68 | } 69 | if len(header) == 0 { 70 | for idx, column := range record { 71 | header[column] = idx 72 | } 73 | continue 74 | } 75 | for column, idx := range header { 76 | result := strings.Split(column, ".") 77 | resource := result[0] 78 | if strings.HasPrefix(resource, "cpu") { 79 | rawValue := record[idx] 80 | value, err := strconv.Atoi(rawValue) 81 | if err != nil { 82 | return cli.NewExitError(fmt.Sprintf("Unable to parse value '%s': %v", rawValue, err), 2) 83 | } 84 | class := result[1] 85 | stat, ok := cpuStats[resource] 86 | if !ok { 87 | stat = cpuStat{values: map[string][]float64{}} 88 | } 89 | values := stat.values[class] 90 | values = append(values, float64(value)) 91 | stat.values[class] = values 92 | cpuStats[resource] = stat 93 | } 94 | if strings.HasPrefix(resource, "int") { 95 | rawValue := record[idx] 96 | value, err := strconv.Atoi(rawValue) 97 | if err != nil { 98 | return cli.NewExitError(fmt.Sprintf("Unable to parse value '%s': %v", rawValue, err), 2) 99 | } 100 | class := result[1] 101 | stat, ok := interruptStats[resource] 102 | if !ok { 103 | stat = interruptStat{values: map[string][]float64{}} 104 | } 105 | values := stat.values[class] 106 | values = append(values, float64(value)) 107 | stat.values[class] = values 108 | interruptStats[resource] = stat 109 | } 110 | if strings.HasPrefix(resource, "softirq") { 111 | resource := result[1] 112 | rawValue := record[idx] 113 | value, err := strconv.Atoi(rawValue) 114 | if err != nil { 115 | return cli.NewExitError(fmt.Sprintf("Unable to parse value '%s': %v", rawValue, err), 2) 116 | } 117 | class := result[2] 118 | stat, ok := softIrqStats[resource] 119 | if !ok { 120 | stat = interruptStat{values: map[string][]float64{}} 121 | } 122 | values := stat.values[class] 123 | values = append(values, float64(value)) 124 | stat.values[class] = values 125 | softIrqStats[resource] = stat 126 | } 127 | } 128 | sampleCount++ 129 | } 130 | fmt.Printf("Processing %s ...\n", filename) 131 | fmt.Printf("\n") 132 | fmt.Printf("N = %d\n", sampleCount) 133 | cpus := []string{} 134 | for cpu, _ := range cpuStats { 135 | cpus = append(cpus, cpu) 136 | } 137 | sort.Strings(cpus) 138 | classes := []string{"system", "usr", "nice", "irq", "softirq", "iowait", "guest", "guestnice", "steal", "idle"} 139 | fmt.Printf("\n") 140 | fmt.Printf("CPU utilization, mean (SD):\n") 141 | fmt.Printf("\n") 142 | fmt.Printf(" ") 143 | for _, class := range classes { 144 | fmt.Printf(" %-12s", class) 145 | } 146 | fmt.Printf("\n") 147 | for _, cpu := range cpus { 148 | fmt.Printf(" %-4s", cpu) 149 | cpuStats := cpuStats[cpu] 150 | for _, class := range classes { 151 | values := cpuStats.values[class] 152 | mean, err := stats.Mean(values) 153 | if err != nil { 154 | return cli.NewExitError(fmt.Sprintf("%v", err), 2) 155 | } 156 | stddev, err := stats.StandardDeviation(values) 157 | if err != nil { 158 | return cli.NewExitError(fmt.Sprintf("%v", err), 2) 159 | } 160 | format := fmt.Sprintf("%.2f (%.2f)", mean, stddev) 161 | fmt.Printf(" %-12s", format) 162 | } 163 | fmt.Printf("\n") 164 | } 165 | fmt.Printf("\n") 166 | if err := printInterrupts("Interrupts", interruptStats, cpus); err != nil { 167 | return err 168 | } 169 | fmt.Printf("\n") 170 | if err := printInterrupts("SoftIRQs", softIrqStats, cpus); err != nil { 171 | return err 172 | } 173 | fmt.Printf("\n") 174 | return nil 175 | } 176 | 177 | func printInterrupts(title string, interruptStats map[string]interruptStat, cpus []string) error { 178 | interrupts := []string{} 179 | for interrupt, _ := range interruptStats { 180 | interrupts = append(interrupts, interrupt) 181 | } 182 | sort.Strings(interrupts) 183 | fmt.Printf("%s, mean (SD):\n", title) 184 | fmt.Printf("\n") 185 | fmt.Printf(" %-10s", "interrupt") 186 | for _, cpu := range cpus { 187 | if cpu == "cpu" { 188 | continue 189 | } 190 | fmt.Printf("%20s", cpu) 191 | } 192 | fmt.Printf("\n") 193 | for _, intr := range interrupts { 194 | interruptStat := interruptStats[intr] 195 | fmt.Printf(" %-10s", intr) 196 | for _, cpu := range cpus { 197 | if cpu == "cpu" { 198 | continue 199 | } 200 | values, ok := interruptStat.values[cpu] 201 | if !ok { 202 | fmt.Printf("%20s", "") 203 | continue 204 | } 205 | mean, err := stats.Mean(values) 206 | if err != nil { 207 | return cli.NewExitError(fmt.Sprintf("%v", err), 2) 208 | } 209 | stddev, err := stats.StandardDeviation(values) 210 | if err != nil { 211 | return cli.NewExitError(fmt.Sprintf("%v", err), 2) 212 | } 213 | format := fmt.Sprintf("%.2f (%.2f)", mean, stddev) 214 | fmt.Printf("%20s", format) 215 | } 216 | fmt.Printf("\n") 217 | } 218 | return nil 219 | } 220 | --------------------------------------------------------------------------------