Browse code

Add the format switch to the stats command

Signed-off-by: Boaz Shuster <ripcurld.github@gmail.com>

Boaz Shuster authored on 2016/07/19 03:30:15
Showing 4 changed files
... ...
@@ -5,25 +5,24 @@ import (
5 5
 	"io"
6 6
 	"strings"
7 7
 	"sync"
8
-	"text/tabwriter"
9 8
 	"time"
10 9
 
11 10
 	"golang.org/x/net/context"
12 11
 
13
-	"github.com/Sirupsen/logrus"
14 12
 	"github.com/docker/docker/api/types"
15 13
 	"github.com/docker/docker/api/types/events"
16 14
 	"github.com/docker/docker/api/types/filters"
17 15
 	"github.com/docker/docker/cli"
18 16
 	"github.com/docker/docker/cli/command"
17
+	"github.com/docker/docker/cli/command/formatter"
19 18
 	"github.com/docker/docker/cli/command/system"
20 19
 	"github.com/spf13/cobra"
21 20
 )
22 21
 
23 22
 type statsOptions struct {
24
-	all      bool
25
-	noStream bool
26
-
23
+	all        bool
24
+	noStream   bool
25
+	format     string
27 26
 	containers []string
28 27
 }
29 28
 
... ...
@@ -44,6 +43,7 @@ func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
44 44
 	flags := cmd.Flags()
45 45
 	flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
46 46
 	flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
47
+	flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
47 48
 	return cmd
48 49
 }
49 50
 
... ...
@@ -98,10 +98,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
98 98
 			closeChan <- err
99 99
 		}
100 100
 		for _, container := range cs {
101
-			s := &containerStats{Name: container.ID[:12]}
101
+			s := formatter.NewContainerStats(container.ID[:12], daemonOSType)
102 102
 			if cStats.add(s) {
103 103
 				waitFirst.Add(1)
104
-				go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
104
+				go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
105 105
 			}
106 106
 		}
107 107
 	}
... ...
@@ -115,19 +115,19 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
115 115
 		eh := system.InitEventHandler()
116 116
 		eh.Handle("create", func(e events.Message) {
117 117
 			if opts.all {
118
-				s := &containerStats{Name: e.ID[:12]}
118
+				s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
119 119
 				if cStats.add(s) {
120 120
 					waitFirst.Add(1)
121
-					go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
121
+					go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
122 122
 				}
123 123
 			}
124 124
 		})
125 125
 
126 126
 		eh.Handle("start", func(e events.Message) {
127
-			s := &containerStats{Name: e.ID[:12]}
127
+			s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
128 128
 			if cStats.add(s) {
129 129
 				waitFirst.Add(1)
130
-				go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
130
+				go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
131 131
 			}
132 132
 		})
133 133
 
... ...
@@ -150,10 +150,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
150 150
 		// Artificially send creation events for the containers we were asked to
151 151
 		// monitor (same code path than we use when monitoring all containers).
152 152
 		for _, name := range opts.containers {
153
-			s := &containerStats{Name: name}
153
+			s := formatter.NewContainerStats(name, daemonOSType)
154 154
 			if cStats.add(s) {
155 155
 				waitFirst.Add(1)
156
-				go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
156
+				go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
157 157
 			}
158 158
 		}
159 159
 
... ...
@@ -166,11 +166,11 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
166 166
 		var errs []string
167 167
 		cStats.mu.Lock()
168 168
 		for _, c := range cStats.cs {
169
-			c.mu.Lock()
170
-			if c.err != nil {
171
-				errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
169
+			c.Mu.Lock()
170
+			if c.Err != nil {
171
+				errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.Err))
172 172
 			}
173
-			c.mu.Unlock()
173
+			c.Mu.Unlock()
174 174
 		}
175 175
 		cStats.mu.Unlock()
176 176
 		if len(errs) > 0 {
... ...
@@ -180,44 +180,34 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
180 180
 
181 181
 	// before print to screen, make sure each container get at least one valid stat data
182 182
 	waitFirst.Wait()
183
+	f := "table"
184
+	if len(opts.format) > 0 {
185
+		f = opts.format
186
+	}
187
+	statsCtx := formatter.Context{
188
+		Output: dockerCli.Out(),
189
+		Format: formatter.NewStatsFormat(f, daemonOSType),
190
+	}
183 191
 
184
-	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
185
-	printHeader := func() {
192
+	cleanHeader := func() {
186 193
 		if !opts.noStream {
187 194
 			fmt.Fprint(dockerCli.Out(), "\033[2J")
188 195
 			fmt.Fprint(dockerCli.Out(), "\033[H")
189 196
 		}
190
-		switch daemonOSType {
191
-		case "":
192
-			// Before we have any stats from the daemon, we don't know the platform...
193
-			io.WriteString(w, "Waiting for statistics...\n")
194
-		case "windows":
195
-			io.WriteString(w, "CONTAINER\tCPU %\tPRIV WORKING SET\tNET I/O\tBLOCK I/O\n")
196
-		default:
197
-			io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n")
198
-		}
199 197
 	}
200 198
 
199
+	var err error
201 200
 	for range time.Tick(500 * time.Millisecond) {
202
-		printHeader()
203
-		toRemove := []string{}
204
-		cStats.mu.Lock()
205
-		for _, s := range cStats.cs {
206
-			if err := s.Display(w); err != nil && !opts.noStream {
207
-				logrus.Debugf("stats: got error for %s: %v", s.Name, err)
208
-				if err == io.EOF {
209
-					toRemove = append(toRemove, s.Name)
210
-				}
211
-			}
212
-		}
213
-		cStats.mu.Unlock()
214
-		for _, name := range toRemove {
215
-			cStats.remove(name)
201
+		cleanHeader()
202
+		cStats.mu.RLock()
203
+		csLen := len(cStats.cs)
204
+		if err = formatter.ContainerStatsWrite(statsCtx, cStats.cs); err != nil {
205
+			break
216 206
 		}
217
-		if len(cStats.cs) == 0 && !showAll {
218
-			return nil
207
+		cStats.mu.RUnlock()
208
+		if csLen == 0 && !showAll {
209
+			break
219 210
 		}
220
-		w.Flush()
221 211
 		if opts.noStream {
222 212
 			break
223 213
 		}
... ...
@@ -237,5 +227,5 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
237 237
 			// just skip
238 238
 		}
239 239
 	}
240
-	return nil
240
+	return err
241 241
 }
... ...
@@ -3,7 +3,6 @@ package container
3 3
 import (
4 4
 	"encoding/json"
5 5
 	"errors"
6
-	"fmt"
7 6
 	"io"
8 7
 	"strings"
9 8
 	"sync"
... ...
@@ -11,30 +10,15 @@ import (
11 11
 
12 12
 	"github.com/Sirupsen/logrus"
13 13
 	"github.com/docker/docker/api/types"
14
+	"github.com/docker/docker/cli/command/formatter"
14 15
 	"github.com/docker/docker/client"
15
-	"github.com/docker/go-units"
16 16
 	"golang.org/x/net/context"
17 17
 )
18 18
 
19
-type containerStats struct {
20
-	Name             string
21
-	CPUPercentage    float64
22
-	Memory           float64 // On Windows this is the private working set
23
-	MemoryLimit      float64 // Not used on Windows
24
-	MemoryPercentage float64 // Not used on Windows
25
-	NetworkRx        float64
26
-	NetworkTx        float64
27
-	BlockRead        float64
28
-	BlockWrite       float64
29
-	PidsCurrent      uint64 // Not used on Windows
30
-	mu               sync.Mutex
31
-	err              error
32
-}
33
-
34 19
 type stats struct {
35
-	mu     sync.Mutex
36 20
 	ostype string
37
-	cs     []*containerStats
21
+	mu     sync.RWMutex
22
+	cs     []*formatter.ContainerStats
38 23
 }
39 24
 
40 25
 // daemonOSType is set once we have at least one stat for a container
... ...
@@ -42,7 +26,7 @@ type stats struct {
42 42
 // on the daemon platform.
43 43
 var daemonOSType string
44 44
 
45
-func (s *stats) add(cs *containerStats) bool {
45
+func (s *stats) add(cs *formatter.ContainerStats) bool {
46 46
 	s.mu.Lock()
47 47
 	defer s.mu.Unlock()
48 48
 	if _, exists := s.isKnownContainer(cs.Name); !exists {
... ...
@@ -69,7 +53,7 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
69 69
 	return -1, false
70 70
 }
71 71
 
72
-func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
72
+func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
73 73
 	logrus.Debugf("collecting stats for %s", s.Name)
74 74
 	var (
75 75
 		getFirst       bool
... ...
@@ -88,9 +72,9 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
88 88
 
89 89
 	response, err := cli.ContainerStats(ctx, s.Name, streamStats)
90 90
 	if err != nil {
91
-		s.mu.Lock()
92
-		s.err = err
93
-		s.mu.Unlock()
91
+		s.Mu.Lock()
92
+		s.Err = err
93
+		s.Mu.Unlock()
94 94
 		return
95 95
 	}
96 96
 	defer response.Body.Close()
... ...
@@ -137,7 +121,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
137 137
 				mem = float64(v.MemoryStats.PrivateWorkingSet)
138 138
 			}
139 139
 
140
-			s.mu.Lock()
140
+			s.Mu.Lock()
141 141
 			s.CPUPercentage = cpuPercent
142 142
 			s.Memory = mem
143 143
 			s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks)
... ...
@@ -148,7 +132,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
148 148
 				s.MemoryPercentage = memPercent
149 149
 				s.PidsCurrent = v.PidsStats.Current
150 150
 			}
151
-			s.mu.Unlock()
151
+			s.Mu.Unlock()
152 152
 			u <- nil
153 153
 			if !streamStats {
154 154
 				return
... ...
@@ -160,7 +144,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
160 160
 		case <-time.After(2 * time.Second):
161 161
 			// zero out the values if we have not received an update within
162 162
 			// the specified duration.
163
-			s.mu.Lock()
163
+			s.Mu.Lock()
164 164
 			s.CPUPercentage = 0
165 165
 			s.Memory = 0
166 166
 			s.MemoryPercentage = 0
... ...
@@ -170,8 +154,8 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
170 170
 			s.BlockRead = 0
171 171
 			s.BlockWrite = 0
172 172
 			s.PidsCurrent = 0
173
-			s.err = errors.New("timeout waiting for stats")
174
-			s.mu.Unlock()
173
+			s.Err = errors.New("timeout waiting for stats")
174
+			s.Mu.Unlock()
175 175
 			// if this is the first stat you get, release WaitGroup
176 176
 			if !getFirst {
177 177
 				getFirst = true
... ...
@@ -179,12 +163,12 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
179 179
 			}
180 180
 		case err := <-u:
181 181
 			if err != nil {
182
-				s.mu.Lock()
183
-				s.err = err
184
-				s.mu.Unlock()
182
+				s.Mu.Lock()
183
+				s.Err = err
184
+				s.Mu.Unlock()
185 185
 				continue
186 186
 			}
187
-			s.err = nil
187
+			s.Err = nil
188 188
 			// if this is the first stat you get, release WaitGroup
189 189
 			if !getFirst {
190 190
 				getFirst = true
... ...
@@ -197,51 +181,6 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
197 197
 	}
198 198
 }
199 199
 
200
-func (s *containerStats) Display(w io.Writer) error {
201
-	s.mu.Lock()
202
-	defer s.mu.Unlock()
203
-	if daemonOSType == "windows" {
204
-		// NOTE: if you change this format, you must also change the err format below!
205
-		format := "%s\t%.2f%%\t%s\t%s / %s\t%s / %s\n"
206
-		if s.err != nil {
207
-			format = "%s\t%s\t%s\t%s / %s\t%s / %s\n"
208
-			errStr := "--"
209
-			fmt.Fprintf(w, format,
210
-				s.Name, errStr, errStr, errStr, errStr, errStr, errStr,
211
-			)
212
-			err := s.err
213
-			return err
214
-		}
215
-		fmt.Fprintf(w, format,
216
-			s.Name,
217
-			s.CPUPercentage,
218
-			units.BytesSize(s.Memory),
219
-			units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3),
220
-			units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3))
221
-	} else {
222
-		// NOTE: if you change this format, you must also change the err format below!
223
-		format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n"
224
-		if s.err != nil {
225
-			format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n"
226
-			errStr := "--"
227
-			fmt.Fprintf(w, format,
228
-				s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr,
229
-			)
230
-			err := s.err
231
-			return err
232
-		}
233
-		fmt.Fprintf(w, format,
234
-			s.Name,
235
-			s.CPUPercentage,
236
-			units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit),
237
-			s.MemoryPercentage,
238
-			units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3),
239
-			units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3),
240
-			s.PidsCurrent)
241
-	}
242
-	return nil
243
-}
244
-
245 200
 func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
246 201
 	var (
247 202
 		cpuPercent = 0.0
... ...
@@ -1,36 +1,11 @@
1 1
 package container
2 2
 
3 3
 import (
4
-	"bytes"
5 4
 	"testing"
6 5
 
7 6
 	"github.com/docker/docker/api/types"
8 7
 )
9 8
 
10
-func TestDisplay(t *testing.T) {
11
-	c := &containerStats{
12
-		Name:             "app",
13
-		CPUPercentage:    30.0,
14
-		Memory:           100 * 1024 * 1024.0,
15
-		MemoryLimit:      2048 * 1024 * 1024.0,
16
-		MemoryPercentage: 100.0 / 2048.0 * 100.0,
17
-		NetworkRx:        100 * 1024 * 1024,
18
-		NetworkTx:        800 * 1024 * 1024,
19
-		BlockRead:        100 * 1024 * 1024,
20
-		BlockWrite:       800 * 1024 * 1024,
21
-		PidsCurrent:      1,
22
-	}
23
-	var b bytes.Buffer
24
-	if err := c.Display(&b); err != nil {
25
-		t.Fatalf("c.Display() gave error: %s", err)
26
-	}
27
-	got := b.String()
28
-	want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t105 MB / 839 MB\t105 MB / 839 MB\t1\n"
29
-	if got != want {
30
-		t.Fatalf("c.Display() = %q, want %q", got, want)
31
-	}
32
-}
33
-
34 9
 func TestCalculBlockIO(t *testing.T) {
35 10
 	blkio := types.BlkioStats{
36 11
 		IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}},
37 12
new file mode 100644
... ...
@@ -0,0 +1,135 @@
0
+package formatter
1
+
2
+import (
3
+	"fmt"
4
+	"sync"
5
+
6
+	"github.com/docker/go-units"
7
+)
8
+
9
+const (
10
+	defaultStatsTableFormat    = "table {{.Container}}\t{{.CPUPrec}}\t{{.MemUsage}}\t{{.MemPrec}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
11
+	winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPrec}}\t{{{.MemUsage}}\t{.NetIO}}\t{{.BlockIO}}"
12
+	emptyStatsTableFormat      = "Waiting for statistics..."
13
+
14
+	containerHeader  = "CONTAINER"
15
+	cpuPrecHeader    = "CPU %"
16
+	netIOHeader      = "NET I/O"
17
+	blockIOHeader    = "BLOCK I/O"
18
+	winMemPrecHeader = "PRIV WORKING SET"  // Used only on Window
19
+	memPrecHeader    = "MEM %"             // Used only on Linux
20
+	memUseHeader     = "MEM USAGE / LIMIT" // Used only on Linux
21
+	pidsHeader       = "PIDS"              // Used only on Linux
22
+)
23
+
24
+// ContainerStatsAttrs represents the statistics data collected from a container.
25
+type ContainerStatsAttrs struct {
26
+	Windows          bool
27
+	Name             string
28
+	CPUPercentage    float64
29
+	Memory           float64 // On Windows this is the private working set
30
+	MemoryLimit      float64 // Not used on Windows
31
+	MemoryPercentage float64 // Not used on Windows
32
+	NetworkRx        float64
33
+	NetworkTx        float64
34
+	BlockRead        float64
35
+	BlockWrite       float64
36
+	PidsCurrent      uint64 // Not used on Windows
37
+}
38
+
39
+// ContainerStats represents the containers statistics data.
40
+type ContainerStats struct {
41
+	Mu sync.RWMutex
42
+	ContainerStatsAttrs
43
+	Err error
44
+}
45
+
46
+// NewStatsFormat returns a format for rendering an CStatsContext
47
+func NewStatsFormat(source, osType string) Format {
48
+	if source == TableFormatKey {
49
+		if osType == "windows" {
50
+			return Format(winDefaultStatsTableFormat)
51
+		}
52
+		return Format(defaultStatsTableFormat)
53
+	}
54
+	return Format(source)
55
+}
56
+
57
+// NewContainerStats returns a new ContainerStats entity and sets in it the given name
58
+func NewContainerStats(name, osType string) *ContainerStats {
59
+	return &ContainerStats{
60
+		ContainerStatsAttrs: ContainerStatsAttrs{
61
+			Name:    name,
62
+			Windows: (osType == "windows"),
63
+		},
64
+	}
65
+}
66
+
67
+// ContainerStatsWrite renders the context for a list of containers statistics
68
+func ContainerStatsWrite(ctx Context, containerStats []*ContainerStats) error {
69
+	render := func(format func(subContext subContext) error) error {
70
+		for _, cstats := range containerStats {
71
+			cstats.Mu.RLock()
72
+			cstatsAttrs := cstats.ContainerStatsAttrs
73
+			cstats.Mu.RUnlock()
74
+			containerStatsCtx := &containerStatsContext{
75
+				s: cstatsAttrs,
76
+			}
77
+			if err := format(containerStatsCtx); err != nil {
78
+				return err
79
+			}
80
+		}
81
+		return nil
82
+	}
83
+	return ctx.Write(&containerStatsContext{}, render)
84
+}
85
+
86
+type containerStatsContext struct {
87
+	HeaderContext
88
+	s ContainerStatsAttrs
89
+}
90
+
91
+func (c *containerStatsContext) Container() string {
92
+	c.AddHeader(containerHeader)
93
+	return c.s.Name
94
+}
95
+
96
+func (c *containerStatsContext) CPUPrec() string {
97
+	c.AddHeader(cpuPrecHeader)
98
+	return fmt.Sprintf("%.2f%%", c.s.CPUPercentage)
99
+}
100
+
101
+func (c *containerStatsContext) MemUsage() string {
102
+	c.AddHeader(memUseHeader)
103
+	if !c.s.Windows {
104
+		return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
105
+	}
106
+	return fmt.Sprintf("-- / --")
107
+}
108
+
109
+func (c *containerStatsContext) MemPrec() string {
110
+	header := memPrecHeader
111
+	if c.s.Windows {
112
+		header = winMemPrecHeader
113
+	}
114
+	c.AddHeader(header)
115
+	return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage)
116
+}
117
+
118
+func (c *containerStatsContext) NetIO() string {
119
+	c.AddHeader(netIOHeader)
120
+	return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3))
121
+}
122
+
123
+func (c *containerStatsContext) BlockIO() string {
124
+	c.AddHeader(blockIOHeader)
125
+	return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3))
126
+}
127
+
128
+func (c *containerStatsContext) PIDs() string {
129
+	c.AddHeader(pidsHeader)
130
+	if !c.s.Windows {
131
+		return fmt.Sprintf("%d", c.s.PidsCurrent)
132
+	}
133
+	return fmt.Sprintf("-")
134
+}