Skip to content

Commit 8a28bee

Browse files
committed
condense output of compose top
This changes the output format of `compose top` and inlines the service container name into the table. Previously, `compose top` had printed something like: <service name> UID PID ... root 1 ... Now, the output looks more like this: SERVICE UID PID ... <name> root 1 ... Signed-off-by: Dominik Menke <[email protected]>
1 parent c01c9c2 commit 8a28bee

File tree

2 files changed

+347
-17
lines changed

2 files changed

+347
-17
lines changed

cmd/compose/top.go

+50-17
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
4949
return topCmd
5050
}
5151

52+
type topHeader map[string]int // maps a proc title to its output index
53+
type topEntries map[string]string
54+
5255
func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
5356
projectName, err := opts.toProjectName(ctx, dockerCli)
5457
if err != nil {
@@ -63,30 +66,60 @@ func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opt
6366
return containers[i].Name < containers[j].Name
6467
})
6568

69+
header, entries := collectTop(containers)
70+
return topPrint(dockerCli.Out(), header, entries)
71+
}
72+
73+
func collectTop(containers []api.ContainerProcSummary) (topHeader, []topEntries) {
74+
// map column name to its header (should keep working if backend.Top returns
75+
// varying columns for different containers)
76+
header := topHeader{"SERVICE": 0}
77+
78+
// assume one process per container and grow if needed
79+
entries := make([]topEntries, 0, len(containers))
80+
6681
for _, container := range containers {
67-
_, _ = fmt.Fprintf(dockerCli.Out(), "%s\n", container.Name)
68-
err := psPrinter(dockerCli.Out(), func(w io.Writer) {
69-
for _, proc := range container.Processes {
70-
info := []interface{}{}
71-
for _, p := range proc {
72-
info = append(info, p)
73-
}
74-
_, _ = fmt.Fprintf(w, strings.Repeat("%s\t", len(info))+"\n", info...)
82+
for _, proc := range container.Processes {
83+
entry := topEntries{"SERVICE": container.Name}
7584

85+
for i, title := range container.Titles {
86+
if _, exists := header[title]; !exists {
87+
header[title] = len(header)
88+
}
89+
entry[title] = proc[i]
7690
}
77-
_, _ = fmt.Fprintln(w)
78-
},
79-
container.Titles...)
80-
if err != nil {
81-
return err
91+
92+
entries = append(entries, entry)
8293
}
8394
}
84-
return nil
95+
return header, entries
8596
}
8697

87-
func psPrinter(out io.Writer, printer func(writer io.Writer), headers ...string) error {
98+
func topPrint(out io.Writer, headers topHeader, rows []topEntries) error {
99+
if len(rows) == 0 {
100+
return nil
101+
}
102+
88103
w := tabwriter.NewWriter(out, 5, 1, 3, ' ', 0)
89-
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
90-
printer(w)
104+
105+
// write headers in the order we've encountered them
106+
h := make([]string, len(headers))
107+
for title, index := range headers {
108+
h[index] = title
109+
}
110+
_, _ = fmt.Fprintln(w, strings.Join(h, "\t"))
111+
112+
for _, row := range rows {
113+
// write proc data in header order
114+
r := make([]string, len(headers))
115+
for title, index := range headers {
116+
if v, ok := row[title]; ok {
117+
r[index] = v
118+
} else {
119+
r[index] = "-"
120+
}
121+
}
122+
_, _ = fmt.Fprintln(w, strings.Join(r, "\t"))
123+
}
91124
return w.Flush()
92125
}

cmd/compose/top_test.go

+297
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
package compose
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
"github.com/docker/compose/v2/pkg/api"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
var topTestCases = []struct {
14+
name string
15+
titles []string
16+
procs [][]string
17+
18+
header topHeader
19+
entries []topEntries
20+
output string
21+
}{
22+
{
23+
name: "noprocs",
24+
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
25+
procs: [][]string{},
26+
header: topHeader{"SERVICE": 0},
27+
entries: []topEntries{},
28+
output: "",
29+
},
30+
{
31+
name: "simple",
32+
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
33+
procs: [][]string{{"root", "1", "1", "0", "12:00", "?", "00:00:01", "/entrypoint"}},
34+
header: topHeader{
35+
"SERVICE": 0,
36+
"UID": 1,
37+
"PID": 2,
38+
"PPID": 3,
39+
"C": 4,
40+
"STIME": 5,
41+
"TTY": 6,
42+
"TIME": 7,
43+
"CMD": 8,
44+
},
45+
entries: []topEntries{
46+
{
47+
"SERVICE": "simple",
48+
"UID": "root",
49+
"PID": "1",
50+
"PPID": "1",
51+
"C": "0",
52+
"STIME": "12:00",
53+
"TTY": "?",
54+
"TIME": "00:00:01",
55+
"CMD": "/entrypoint",
56+
},
57+
},
58+
output: trim(`
59+
SERVICE UID PID PPID C STIME TTY TIME CMD
60+
simple root 1 1 0 12:00 ? 00:00:01 /entrypoint
61+
`),
62+
},
63+
{
64+
name: "noppid",
65+
titles: []string{"UID", "PID", "C", "STIME", "TTY", "TIME", "CMD"},
66+
procs: [][]string{{"root", "1", "0", "12:00", "?", "00:00:02", "/entrypoint"}},
67+
header: topHeader{
68+
"SERVICE": 0,
69+
"UID": 1,
70+
"PID": 2,
71+
"C": 3,
72+
"STIME": 4,
73+
"TTY": 5,
74+
"TIME": 6,
75+
"CMD": 7,
76+
},
77+
entries: []topEntries{
78+
{
79+
"SERVICE": "noppid",
80+
"UID": "root",
81+
"PID": "1",
82+
"C": "0",
83+
"STIME": "12:00",
84+
"TTY": "?",
85+
"TIME": "00:00:02",
86+
"CMD": "/entrypoint",
87+
},
88+
},
89+
output: trim(`
90+
SERVICE UID PID C STIME TTY TIME CMD
91+
noppid root 1 0 12:00 ? 00:00:02 /entrypoint
92+
`),
93+
},
94+
{
95+
name: "extra-hdr",
96+
titles: []string{"UID", "GID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
97+
procs: [][]string{{"root", "1", "1", "1", "0", "12:00", "?", "00:00:03", "/entrypoint"}},
98+
header: topHeader{
99+
"SERVICE": 0,
100+
"UID": 1,
101+
"GID": 2,
102+
"PID": 3,
103+
"PPID": 4,
104+
"C": 5,
105+
"STIME": 6,
106+
"TTY": 7,
107+
"TIME": 8,
108+
"CMD": 9,
109+
},
110+
entries: []topEntries{
111+
{
112+
"SERVICE": "extra-hdr",
113+
"UID": "root",
114+
"GID": "1",
115+
"PID": "1",
116+
"PPID": "1",
117+
"C": "0",
118+
"STIME": "12:00",
119+
"TTY": "?",
120+
"TIME": "00:00:03",
121+
"CMD": "/entrypoint",
122+
},
123+
},
124+
output: trim(`
125+
SERVICE UID GID PID PPID C STIME TTY TIME CMD
126+
extra-hdr root 1 1 1 0 12:00 ? 00:00:03 /entrypoint
127+
`),
128+
},
129+
{
130+
name: "multiple",
131+
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
132+
procs: [][]string{
133+
{"root", "1", "1", "0", "12:00", "?", "00:00:04", "/entrypoint"},
134+
{"root", "123", "1", "0", "12:00", "?", "00:00:42", "sleep infinity"},
135+
},
136+
header: topHeader{
137+
"SERVICE": 0,
138+
"UID": 1,
139+
"PID": 2,
140+
"PPID": 3,
141+
"C": 4,
142+
"STIME": 5,
143+
"TTY": 6,
144+
"TIME": 7,
145+
"CMD": 8,
146+
},
147+
entries: []topEntries{
148+
{
149+
"SERVICE": "multiple",
150+
"UID": "root",
151+
"PID": "1",
152+
"PPID": "1",
153+
"C": "0",
154+
"STIME": "12:00",
155+
"TTY": "?",
156+
"TIME": "00:00:04",
157+
"CMD": "/entrypoint",
158+
},
159+
{
160+
"SERVICE": "multiple",
161+
"UID": "root",
162+
"PID": "123",
163+
"PPID": "1",
164+
"C": "0",
165+
"STIME": "12:00",
166+
"TTY": "?",
167+
"TIME": "00:00:42",
168+
"CMD": "sleep infinity",
169+
},
170+
},
171+
output: trim(`
172+
SERVICE UID PID PPID C STIME TTY TIME CMD
173+
multiple root 1 1 0 12:00 ? 00:00:04 /entrypoint
174+
multiple root 123 1 0 12:00 ? 00:00:42 sleep infinity
175+
`),
176+
},
177+
}
178+
179+
// TestRunTopCore only tests the core functionality of runTop: formatting
180+
// and printing of the output of (api.Service).Top().
181+
func TestRunTopCore(t *testing.T) {
182+
t.Parallel()
183+
184+
all := []api.ContainerProcSummary{}
185+
186+
for _, tc := range topTestCases {
187+
summary := api.ContainerProcSummary{
188+
Name: tc.name,
189+
Titles: tc.titles,
190+
Processes: tc.procs,
191+
}
192+
all = append(all, summary)
193+
194+
t.Run(tc.name, func(t *testing.T) {
195+
header, entries := collectTop([]api.ContainerProcSummary{summary})
196+
assert.EqualValues(t, tc.header, header)
197+
assert.EqualValues(t, tc.entries, entries)
198+
199+
var buf bytes.Buffer
200+
err := topPrint(&buf, header, entries)
201+
202+
require.NoError(t, err)
203+
assert.Equal(t, tc.output, buf.String())
204+
})
205+
}
206+
207+
t.Run("all", func(t *testing.T) {
208+
header, entries := collectTop(all)
209+
assert.EqualValues(t, topHeader{
210+
"SERVICE": 0,
211+
"UID": 1,
212+
"PID": 2,
213+
"PPID": 3,
214+
"C": 4,
215+
"STIME": 5,
216+
"TTY": 6,
217+
"TIME": 7,
218+
"CMD": 8,
219+
"GID": 9,
220+
}, header)
221+
assert.EqualValues(t, []topEntries{
222+
{
223+
"SERVICE": "simple",
224+
"UID": "root",
225+
"PID": "1",
226+
"PPID": "1",
227+
"C": "0",
228+
"STIME": "12:00",
229+
"TTY": "?",
230+
"TIME": "00:00:01",
231+
"CMD": "/entrypoint",
232+
}, {
233+
"SERVICE": "noppid",
234+
"UID": "root",
235+
"PID": "1",
236+
"C": "0",
237+
"STIME": "12:00",
238+
"TTY": "?",
239+
"TIME": "00:00:02",
240+
"CMD": "/entrypoint",
241+
}, {
242+
"SERVICE": "extra-hdr",
243+
"UID": "root",
244+
"GID": "1",
245+
"PID": "1",
246+
"PPID": "1",
247+
"C": "0",
248+
"STIME": "12:00",
249+
"TTY": "?",
250+
"TIME": "00:00:03",
251+
"CMD": "/entrypoint",
252+
}, {
253+
"SERVICE": "multiple",
254+
"UID": "root",
255+
"PID": "1",
256+
"PPID": "1",
257+
"C": "0",
258+
"STIME": "12:00",
259+
"TTY": "?",
260+
"TIME": "00:00:04",
261+
"CMD": "/entrypoint",
262+
}, {
263+
"SERVICE": "multiple",
264+
"UID": "root",
265+
"PID": "123",
266+
"PPID": "1",
267+
"C": "0",
268+
"STIME": "12:00",
269+
"TTY": "?",
270+
"TIME": "00:00:42",
271+
"CMD": "sleep infinity",
272+
},
273+
}, entries)
274+
275+
var buf bytes.Buffer
276+
err := topPrint(&buf, header, entries)
277+
require.NoError(t, err)
278+
assert.Equal(t, trim(`
279+
SERVICE UID PID PPID C STIME TTY TIME CMD GID
280+
simple root 1 1 0 12:00 ? 00:00:01 /entrypoint -
281+
noppid root 1 - 0 12:00 ? 00:00:02 /entrypoint -
282+
extra-hdr root 1 1 0 12:00 ? 00:00:03 /entrypoint 1
283+
multiple root 1 1 0 12:00 ? 00:00:04 /entrypoint -
284+
multiple root 123 1 0 12:00 ? 00:00:42 sleep infinity -
285+
`), buf.String())
286+
287+
})
288+
}
289+
290+
func trim(s string) string {
291+
var out bytes.Buffer
292+
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
293+
out.WriteString(strings.TrimSpace(line))
294+
out.WriteRune('\n')
295+
}
296+
return out.String()
297+
}

0 commit comments

Comments
 (0)