Skip to content

Commit

Permalink
condense output of compose top
Browse files Browse the repository at this point in the history
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     ...
  • Loading branch information
dmke committed Dec 16, 2024
1 parent c01c9c2 commit f26757a
Show file tree
Hide file tree
Showing 2 changed files with 347 additions and 17 deletions.
67 changes: 50 additions & 17 deletions cmd/compose/top.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
return topCmd
}

type topHeader map[string]int // maps a proc title to its output index
type topEntries map[string]string

func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error {
projectName, err := opts.toProjectName(ctx, dockerCli)
if err != nil {
Expand All @@ -63,30 +66,60 @@ func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opt
return containers[i].Name < containers[j].Name
})

header, entries := collectTop(containers)
return topPrint(dockerCli.Out(), header, entries)
}

func collectTop(containers []api.ContainerProcSummary) (topHeader, []topEntries) {
// map column name to its header (should keep working if backend.Top returns
// varying columns for different containers)
header := topHeader{"SERVICE": 0}

// assume one process per container and grow if needed
entries := make([]topEntries, 0, len(containers))

for _, container := range containers {
_, _ = fmt.Fprintf(dockerCli.Out(), "%s\n", container.Name)
err := psPrinter(dockerCli.Out(), func(w io.Writer) {
for _, proc := range container.Processes {
info := []interface{}{}
for _, p := range proc {
info = append(info, p)
}
_, _ = fmt.Fprintf(w, strings.Repeat("%s\t", len(info))+"\n", info...)
for _, proc := range container.Processes {
entry := topEntries{"SERVICE": container.Name}

for i, title := range container.Titles {
if _, exists := header[title]; !exists {
header[title] = len(header)
}
entry[title] = proc[i]
}
_, _ = fmt.Fprintln(w)
},
container.Titles...)
if err != nil {
return err

entries = append(entries, entry)
}
}
return nil
return header, entries
}

func psPrinter(out io.Writer, printer func(writer io.Writer), headers ...string) error {
func topPrint(out io.Writer, headers topHeader, rows []topEntries) error {
if len(rows) == 0 {
return nil
}

w := tabwriter.NewWriter(out, 5, 1, 3, ' ', 0)
_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
printer(w)

// write headers in the order we've encountered them
h := make([]string, len(headers))
for title, index := range headers {
h[index] = title
}
_, _ = fmt.Fprintln(w, strings.Join(h, "\t"))

for _, row := range rows {
// write proc data in header order
r := make([]string, len(headers))
for title, index := range headers {
if v, ok := row[title]; ok {
r[index] = v
} else {
r[index] = "-"
}
}
_, _ = fmt.Fprintln(w, strings.Join(r, "\t"))
}
return w.Flush()
}
297 changes: 297 additions & 0 deletions cmd/compose/top_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
package compose

import (
"bytes"
"strings"
"testing"

"github.com/docker/compose/v2/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var topTestCases = []struct {
name string
titles []string
procs [][]string

header topHeader
entries []topEntries
output string
}{
{
name: "noprocs",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{},
header: topHeader{"SERVICE": 0},
entries: []topEntries{},
output: "",
},
{
name: "simple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "0", "12:00", "?", "00:00:01", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"UID": 1,
"PID": 2,
"PPID": 3,
"C": 4,
"STIME": 5,
"TTY": 6,
"TIME": 7,
"CMD": 8,
},
entries: []topEntries{
{
"SERVICE": "simple",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE UID PID PPID C STIME TTY TIME CMD
simple root 1 1 0 12:00 ? 00:00:01 /entrypoint
`),
},
{
name: "noppid",
titles: []string{"UID", "PID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "0", "12:00", "?", "00:00:02", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"UID": 1,
"PID": 2,
"C": 3,
"STIME": 4,
"TTY": 5,
"TIME": 6,
"CMD": 7,
},
entries: []topEntries{
{
"SERVICE": "noppid",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE UID PID C STIME TTY TIME CMD
noppid root 1 0 12:00 ? 00:00:02 /entrypoint
`),
},
{
name: "extra-hdr",
titles: []string{"UID", "GID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{{"root", "1", "1", "1", "0", "12:00", "?", "00:00:03", "/entrypoint"}},
header: topHeader{
"SERVICE": 0,
"UID": 1,
"GID": 2,
"PID": 3,
"PPID": 4,
"C": 5,
"STIME": 6,
"TTY": 7,
"TIME": 8,
"CMD": 9,
},
entries: []topEntries{
{
"SERVICE": "extra-hdr",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
},
},
output: trim(`
SERVICE UID GID PID PPID C STIME TTY TIME CMD
extra-hdr root 1 1 1 0 12:00 ? 00:00:03 /entrypoint
`),
},
{
name: "multiple",
titles: []string{"UID", "PID", "PPID", "C", "STIME", "TTY", "TIME", "CMD"},
procs: [][]string{
{"root", "1", "1", "0", "12:00", "?", "00:00:04", "/entrypoint"},
{"root", "123", "1", "0", "12:00", "?", "00:00:42", "sleep infinity"},
},
header: topHeader{
"SERVICE": 0,
"UID": 1,
"PID": 2,
"PPID": 3,
"C": 4,
"STIME": 5,
"TTY": 6,
"TIME": 7,
"CMD": 8,
},
entries: []topEntries{
{
"SERVICE": "multiple",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
},
{
"SERVICE": "multiple",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
},
output: trim(`
SERVICE UID PID PPID C STIME TTY TIME CMD
multiple root 1 1 0 12:00 ? 00:00:04 /entrypoint
multiple root 123 1 0 12:00 ? 00:00:42 sleep infinity
`),
},
}

// TestRunTopCore only tests the core functionality of runTop: formatting
// and printing of the output of (api.Service).Top().
func TestRunTopCore(t *testing.T) {
t.Parallel()

all := []api.ContainerProcSummary{}

for _, tc := range topTestCases {
summary := api.ContainerProcSummary{
Name: tc.name,
Titles: tc.titles,
Processes: tc.procs,
}
all = append(all, summary)

t.Run(tc.name, func(t *testing.T) {
header, entries := collectTop([]api.ContainerProcSummary{summary})
assert.EqualValues(t, tc.header, header)
assert.EqualValues(t, tc.entries, entries)

var buf bytes.Buffer
err := topPrint(&buf, header, entries)

require.NoError(t, err)
assert.Equal(t, tc.output, buf.String())
})
}

t.Run("all", func(t *testing.T) {
header, entries := collectTop(all)
assert.EqualValues(t, topHeader{
"SERVICE": 0,
"UID": 1,
"PID": 2,
"PPID": 3,
"C": 4,
"STIME": 5,
"TTY": 6,
"TIME": 7,
"CMD": 8,
"GID": 9,
}, header)
assert.EqualValues(t, []topEntries{
{
"SERVICE": "simple",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:01",
"CMD": "/entrypoint",
}, {
"SERVICE": "noppid",
"UID": "root",
"PID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:02",
"CMD": "/entrypoint",
}, {
"SERVICE": "extra-hdr",
"UID": "root",
"GID": "1",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:03",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"UID": "root",
"PID": "1",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:04",
"CMD": "/entrypoint",
}, {
"SERVICE": "multiple",
"UID": "root",
"PID": "123",
"PPID": "1",
"C": "0",
"STIME": "12:00",
"TTY": "?",
"TIME": "00:00:42",
"CMD": "sleep infinity",
},
}, entries)

var buf bytes.Buffer
err := topPrint(&buf, header, entries)
require.NoError(t, err)
assert.Equal(t, trim(`
SERVICE UID PID PPID C STIME TTY TIME CMD GID
simple root 1 1 0 12:00 ? 00:00:01 /entrypoint -
noppid root 1 - 0 12:00 ? 00:00:02 /entrypoint -
extra-hdr root 1 1 0 12:00 ? 00:00:03 /entrypoint 1
multiple root 1 1 0 12:00 ? 00:00:04 /entrypoint -
multiple root 123 1 0 12:00 ? 00:00:42 sleep infinity -
`), buf.String())

})
}

func trim(s string) string {
var out bytes.Buffer
for _, line := range strings.Split(strings.TrimSpace(s), "\n") {
out.WriteString(strings.TrimSpace(line))
out.WriteRune('\n')
}
return out.String()
}

0 comments on commit f26757a

Please sign in to comment.