Skip to content

Commit

Permalink
Merge pull request moby#24987 from ripcurld00d/stats_format_prod
Browse files Browse the repository at this point in the history
Add format to stats
  • Loading branch information
dnephin authored Sep 21, 2016
2 parents 1385ad8 + a4f3442 commit 685613f
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 149 deletions.
82 changes: 36 additions & 46 deletions command/container/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@ import (
"io"
"strings"
"sync"
"text/tabwriter"
"time"

"golang.org/x/net/context"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/formatter"
"github.com/docker/docker/cli/command/system"
"github.com/spf13/cobra"
)

type statsOptions struct {
all bool
noStream bool

all bool
noStream bool
format string
containers []string
}

Expand All @@ -44,6 +43,7 @@ func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
return cmd
}

Expand Down Expand Up @@ -98,10 +98,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
closeChan <- err
}
for _, container := range cs {
s := &containerStats{Name: container.ID[:12]}
s := formatter.NewContainerStats(container.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
}
}
}
Expand All @@ -115,19 +115,19 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
eh := system.InitEventHandler()
eh.Handle("create", func(e events.Message) {
if opts.all {
s := &containerStats{Name: e.ID[:12]}
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
}
}
})

eh.Handle("start", func(e events.Message) {
s := &containerStats{Name: e.ID[:12]}
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
}
})

Expand All @@ -150,10 +150,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
// Artificially send creation events for the containers we were asked to
// monitor (same code path than we use when monitoring all containers).
for _, name := range opts.containers {
s := &containerStats{Name: name}
s := formatter.NewContainerStats(name, daemonOSType)
if cStats.add(s) {
waitFirst.Add(1)
go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst)
go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst)
}
}

Expand All @@ -166,11 +166,11 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
var errs []string
cStats.mu.Lock()
for _, c := range cStats.cs {
c.mu.Lock()
if c.err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err))
c.Mu.Lock()
if c.Err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.Err))
}
c.mu.Unlock()
c.Mu.Unlock()
}
cStats.mu.Unlock()
if len(errs) > 0 {
Expand All @@ -180,44 +180,34 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {

// before print to screen, make sure each container get at least one valid stat data
waitFirst.Wait()
f := "table"
if len(opts.format) > 0 {
f = opts.format
}
statsCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewStatsFormat(f, daemonOSType),
}

w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
printHeader := func() {
cleanHeader := func() {
if !opts.noStream {
fmt.Fprint(dockerCli.Out(), "\033[2J")
fmt.Fprint(dockerCli.Out(), "\033[H")
}
switch daemonOSType {
case "":
// Before we have any stats from the daemon, we don't know the platform...
io.WriteString(w, "Waiting for statistics...\n")
case "windows":
io.WriteString(w, "CONTAINER\tCPU %\tPRIV WORKING SET\tNET I/O\tBLOCK I/O\n")
default:
io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n")
}
}

var err error
for range time.Tick(500 * time.Millisecond) {
printHeader()
toRemove := []string{}
cStats.mu.Lock()
for _, s := range cStats.cs {
if err := s.Display(w); err != nil && !opts.noStream {
logrus.Debugf("stats: got error for %s: %v", s.Name, err)
if err == io.EOF {
toRemove = append(toRemove, s.Name)
}
}
}
cStats.mu.Unlock()
for _, name := range toRemove {
cStats.remove(name)
cleanHeader()
cStats.mu.RLock()
csLen := len(cStats.cs)
if err = formatter.ContainerStatsWrite(statsCtx, cStats.cs); err != nil {
break
}
if len(cStats.cs) == 0 && !showAll {
return nil
cStats.mu.RUnlock()
if csLen == 0 && !showAll {
break
}
w.Flush()
if opts.noStream {
break
}
Expand All @@ -237,5 +227,5 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
// just skip
}
}
return nil
return err
}
95 changes: 17 additions & 78 deletions command/container/stats_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,30 @@ package container
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"sync"
"time"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli/command/formatter"
"github.com/docker/docker/client"
"github.com/docker/go-units"
"golang.org/x/net/context"
)

type containerStats struct {
Name string
CPUPercentage float64
Memory float64 // On Windows this is the private working set
MemoryLimit float64 // Not used on Windows
MemoryPercentage float64 // Not used on Windows
NetworkRx float64
NetworkTx float64
BlockRead float64
BlockWrite float64
PidsCurrent uint64 // Not used on Windows
mu sync.Mutex
err error
}

type stats struct {
mu sync.Mutex
ostype string
cs []*containerStats
mu sync.RWMutex
cs []*formatter.ContainerStats
}

// daemonOSType is set once we have at least one stat for a container
// from the daemon. It is used to ensure we print the right header based
// on the daemon platform.
var daemonOSType string

func (s *stats) add(cs *containerStats) bool {
func (s *stats) add(cs *formatter.ContainerStats) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.isKnownContainer(cs.Name); !exists {
Expand All @@ -69,7 +53,7 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
return -1, false
}

func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) {
logrus.Debugf("collecting stats for %s", s.Name)
var (
getFirst bool
Expand All @@ -88,9 +72,9 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre

response, err := cli.ContainerStats(ctx, s.Name, streamStats)
if err != nil {
s.mu.Lock()
s.err = err
s.mu.Unlock()
s.Mu.Lock()
s.Err = err
s.Mu.Unlock()
return
}
defer response.Body.Close()
Expand Down Expand Up @@ -137,7 +121,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
mem = float64(v.MemoryStats.PrivateWorkingSet)
}

s.mu.Lock()
s.Mu.Lock()
s.CPUPercentage = cpuPercent
s.Memory = mem
s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks)
Expand All @@ -148,7 +132,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
s.MemoryPercentage = memPercent
s.PidsCurrent = v.PidsStats.Current
}
s.mu.Unlock()
s.Mu.Unlock()
u <- nil
if !streamStats {
return
Expand All @@ -160,7 +144,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
case <-time.After(2 * time.Second):
// zero out the values if we have not received an update within
// the specified duration.
s.mu.Lock()
s.Mu.Lock()
s.CPUPercentage = 0
s.Memory = 0
s.MemoryPercentage = 0
Expand All @@ -170,21 +154,21 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
s.BlockRead = 0
s.BlockWrite = 0
s.PidsCurrent = 0
s.err = errors.New("timeout waiting for stats")
s.mu.Unlock()
s.Err = errors.New("timeout waiting for stats")
s.Mu.Unlock()
// if this is the first stat you get, release WaitGroup
if !getFirst {
getFirst = true
waitFirst.Done()
}
case err := <-u:
if err != nil {
s.mu.Lock()
s.err = err
s.mu.Unlock()
s.Mu.Lock()
s.Err = err
s.Mu.Unlock()
continue
}
s.err = nil
s.Err = nil
// if this is the first stat you get, release WaitGroup
if !getFirst {
getFirst = true
Expand All @@ -197,51 +181,6 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre
}
}

func (s *containerStats) Display(w io.Writer) error {
s.mu.Lock()
defer s.mu.Unlock()
if daemonOSType == "windows" {
// NOTE: if you change this format, you must also change the err format below!
format := "%s\t%.2f%%\t%s\t%s / %s\t%s / %s\n"
if s.err != nil {
format = "%s\t%s\t%s\t%s / %s\t%s / %s\n"
errStr := "--"
fmt.Fprintf(w, format,
s.Name, errStr, errStr, errStr, errStr, errStr, errStr,
)
err := s.err
return err
}
fmt.Fprintf(w, format,
s.Name,
s.CPUPercentage,
units.BytesSize(s.Memory),
units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3),
units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3))
} else {
// NOTE: if you change this format, you must also change the err format below!
format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n"
if s.err != nil {
format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n"
errStr := "--"
fmt.Fprintf(w, format,
s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr,
)
err := s.err
return err
}
fmt.Fprintf(w, format,
s.Name,
s.CPUPercentage,
units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit),
s.MemoryPercentage,
units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3),
units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3),
s.PidsCurrent)
}
return nil
}

func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
var (
cpuPercent = 0.0
Expand Down
25 changes: 0 additions & 25 deletions command/container/stats_unit_test.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,11 @@
package container

import (
"bytes"
"testing"

"github.com/docker/docker/api/types"
)

func TestDisplay(t *testing.T) {
c := &containerStats{
Name: "app",
CPUPercentage: 30.0,
Memory: 100 * 1024 * 1024.0,
MemoryLimit: 2048 * 1024 * 1024.0,
MemoryPercentage: 100.0 / 2048.0 * 100.0,
NetworkRx: 100 * 1024 * 1024,
NetworkTx: 800 * 1024 * 1024,
BlockRead: 100 * 1024 * 1024,
BlockWrite: 800 * 1024 * 1024,
PidsCurrent: 1,
}
var b bytes.Buffer
if err := c.Display(&b); err != nil {
t.Fatalf("c.Display() gave error: %s", err)
}
got := b.String()
want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t105 MB / 839 MB\t105 MB / 839 MB\t1\n"
if got != want {
t.Fatalf("c.Display() = %q, want %q", got, want)
}
}

func TestCalculBlockIO(t *testing.T) {
blkio := types.BlkioStats{
IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}},
Expand Down
Loading

0 comments on commit 685613f

Please sign in to comment.