stats.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. package container
  2. import (
  3. "fmt"
  4. "io"
  5. "strings"
  6. "sync"
  7. "time"
  8. "golang.org/x/net/context"
  9. "github.com/docker/docker/api/types"
  10. "github.com/docker/docker/api/types/events"
  11. "github.com/docker/docker/api/types/filters"
  12. "github.com/docker/docker/cli"
  13. "github.com/docker/docker/cli/command"
  14. "github.com/docker/docker/cli/command/formatter"
  15. "github.com/docker/docker/cli/command/system"
  16. "github.com/spf13/cobra"
  17. )
  18. type statsOptions struct {
  19. all bool
  20. noStream bool
  21. format string
  22. containers []string
  23. }
  24. // NewStatsCommand creates a new cobra.Command for `docker stats`
  25. func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
  26. var opts statsOptions
  27. cmd := &cobra.Command{
  28. Use: "stats [OPTIONS] [CONTAINER...]",
  29. Short: "Display a live stream of container(s) resource usage statistics",
  30. Args: cli.RequiresMinArgs(0),
  31. RunE: func(cmd *cobra.Command, args []string) error {
  32. opts.containers = args
  33. return runStats(dockerCli, &opts)
  34. },
  35. }
  36. flags := cmd.Flags()
  37. flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
  38. flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
  39. flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
  40. return cmd
  41. }
  42. // runStats displays a live stream of resource usage statistics for one or more containers.
  43. // This shows real-time information on CPU usage, memory usage, and network I/O.
  44. func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
  45. showAll := len(opts.containers) == 0
  46. closeChan := make(chan error)
  47. ctx := context.Background()
  48. // monitorContainerEvents watches for container creation and removal (only
  49. // used when calling `docker stats` without arguments).
  50. monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) {
  51. f := filters.NewArgs()
  52. f.Add("type", "container")
  53. options := types.EventsOptions{
  54. Filters: f,
  55. }
  56. resBody, err := dockerCli.Client().Events(ctx, options)
  57. // Whether we successfully subscribed to events or not, we can now
  58. // unblock the main goroutine.
  59. close(started)
  60. if err != nil {
  61. closeChan <- err
  62. return
  63. }
  64. defer resBody.Close()
  65. system.DecodeEvents(resBody, func(event events.Message, err error) error {
  66. if err != nil {
  67. closeChan <- err
  68. return nil
  69. }
  70. c <- event
  71. return nil
  72. })
  73. }
  74. // waitFirst is a WaitGroup to wait first stat data's reach for each container
  75. waitFirst := &sync.WaitGroup{}
  76. cStats := stats{}
  77. // getContainerList simulates creation event for all previously existing
  78. // containers (only used when calling `docker stats` without arguments).
  79. getContainerList := func() {
  80. options := types.ContainerListOptions{
  81. All: opts.all,
  82. }
  83. cs, err := dockerCli.Client().ContainerList(ctx, options)
  84. if err != nil {
  85. closeChan <- err
  86. }
  87. for _, container := range cs {
  88. s := formatter.NewContainerStats(container.ID[:12], daemonOSType)
  89. if cStats.add(s) {
  90. waitFirst.Add(1)
  91. go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
  92. }
  93. }
  94. }
  95. if showAll {
  96. // If no names were specified, start a long running goroutine which
  97. // monitors container events. We make sure we're subscribed before
  98. // retrieving the list of running containers to avoid a race where we
  99. // would "miss" a creation.
  100. started := make(chan struct{})
  101. eh := system.InitEventHandler()
  102. eh.Handle("create", func(e events.Message) {
  103. if opts.all {
  104. s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
  105. if cStats.add(s) {
  106. waitFirst.Add(1)
  107. go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
  108. }
  109. }
  110. })
  111. eh.Handle("start", func(e events.Message) {
  112. s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
  113. if cStats.add(s) {
  114. waitFirst.Add(1)
  115. go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
  116. }
  117. })
  118. eh.Handle("die", func(e events.Message) {
  119. if !opts.all {
  120. cStats.remove(e.ID[:12])
  121. }
  122. })
  123. eventChan := make(chan events.Message)
  124. go eh.Watch(eventChan)
  125. go monitorContainerEvents(started, eventChan)
  126. defer close(eventChan)
  127. <-started
  128. // Start a short-lived goroutine to retrieve the initial list of
  129. // containers.
  130. getContainerList()
  131. } else {
  132. // Artificially send creation events for the containers we were asked to
  133. // monitor (same code path than we use when monitoring all containers).
  134. for _, name := range opts.containers {
  135. s := formatter.NewContainerStats(name, daemonOSType)
  136. if cStats.add(s) {
  137. waitFirst.Add(1)
  138. go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
  139. }
  140. }
  141. // We don't expect any asynchronous errors: closeChan can be closed.
  142. close(closeChan)
  143. // Do a quick pause to detect any error with the provided list of
  144. // container names.
  145. time.Sleep(1500 * time.Millisecond)
  146. var errs []string
  147. cStats.mu.Lock()
  148. for _, c := range cStats.cs {
  149. c.Mu.Lock()
  150. if c.Err != nil {
  151. errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.Err))
  152. }
  153. c.Mu.Unlock()
  154. }
  155. cStats.mu.Unlock()
  156. if len(errs) > 0 {
  157. return fmt.Errorf("%s", strings.Join(errs, ", "))
  158. }
  159. }
  160. // before print to screen, make sure each container get at least one valid stat data
  161. waitFirst.Wait()
  162. f := "table"
  163. if len(opts.format) > 0 {
  164. f = opts.format
  165. }
  166. statsCtx := formatter.Context{
  167. Output: dockerCli.Out(),
  168. Format: formatter.NewStatsFormat(f, daemonOSType),
  169. }
  170. cleanHeader := func() {
  171. if !opts.noStream {
  172. fmt.Fprint(dockerCli.Out(), "\033[2J")
  173. fmt.Fprint(dockerCli.Out(), "\033[H")
  174. }
  175. }
  176. var err error
  177. for range time.Tick(500 * time.Millisecond) {
  178. cleanHeader()
  179. cStats.mu.RLock()
  180. csLen := len(cStats.cs)
  181. if err = formatter.ContainerStatsWrite(statsCtx, cStats.cs); err != nil {
  182. break
  183. }
  184. cStats.mu.RUnlock()
  185. if csLen == 0 && !showAll {
  186. break
  187. }
  188. if opts.noStream {
  189. break
  190. }
  191. select {
  192. case err, ok := <-closeChan:
  193. if ok {
  194. if err != nil {
  195. // this is suppressing "unexpected EOF" in the cli when the
  196. // daemon restarts so it shutdowns cleanly
  197. if err == io.ErrUnexpectedEOF {
  198. return nil
  199. }
  200. return err
  201. }
  202. }
  203. default:
  204. // just skip
  205. }
  206. }
  207. return err
  208. }