diff --git a/api/client/stats.go b/api/client/stats.go index 06ec3039df..896358c70b 100644 --- a/api/client/stats.go +++ b/api/client/stats.go @@ -56,7 +56,7 @@ func (s *containerStats) Collect(cli *DockerCli, streamStats bool) { ) go func() { for { - var v *types.Stats + var v *types.StatsJSON if err := dec.Decode(&v); err != nil { u <- err return @@ -80,8 +80,7 @@ func (s *containerStats) Collect(cli *DockerCli, streamStats bool) { s.Memory = float64(v.MemoryStats.Usage) s.MemoryLimit = float64(v.MemoryStats.Limit) s.MemoryPercentage = memPercent - s.NetworkRx = float64(v.Network.RxBytes) - s.NetworkTx = float64(v.Network.TxBytes) + s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks) s.BlockRead = float64(blkRead) s.BlockWrite = float64(blkWrite) s.mu.Unlock() @@ -198,7 +197,7 @@ func (cli *DockerCli) CmdStats(args ...string) error { return nil } -func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.Stats) float64 { +func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { var ( cpuPercent = 0.0 // calculate the change for the cpu usage of the container in between readings @@ -224,3 +223,13 @@ func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) } return } + +func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) { + var rx, tx float64 + + for _, v := range network { + rx += float64(v.RxBytes) + tx += float64(v.TxBytes) + } + return rx, tx +} diff --git a/api/server/container.go b/api/server/container.go index 419160d231..09cae79805 100644 --- a/api/server/container.go +++ b/api/server/container.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/daemon" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/version" "github.com/docker/docker/runconfig" ) @@ -81,10 +82,12 @@ func (s *Server) getContainersStats(ctx context.Context, w http.ResponseWriter, closeNotifier = notifier.CloseNotify() } + version, _ := ctx.Value("api-version").(version.Version) config := &daemon.ContainerStatsConfig{ Stream: stream, OutStream: out, Stop: closeNotifier, + Version: version, } return s.daemon.ContainerStats(container, config) diff --git a/api/types/stats.go b/api/types/stats.go index f2819cacba..f1395f6e9c 100644 --- a/api/types/stats.go +++ b/api/types/stats.go @@ -89,10 +89,25 @@ type NetworkStats struct { // Stats is Ultimate struct aggregating all types of stats of one container type Stats struct { - Read time.Time `json:"read"` - Network NetworkStats `json:"network,omitempty"` - PreCPUStats CPUStats `json:"precpu_stats,omitempty"` - CPUStats CPUStats `json:"cpu_stats,omitempty"` - MemoryStats MemoryStats `json:"memory_stats,omitempty"` - BlkioStats BlkioStats `json:"blkio_stats,omitempty"` + Read time.Time `json:"read"` + PreCPUStats CPUStats `json:"precpu_stats,omitempty"` + CPUStats CPUStats `json:"cpu_stats,omitempty"` + MemoryStats MemoryStats `json:"memory_stats,omitempty"` + BlkioStats BlkioStats `json:"blkio_stats,omitempty"` +} + +// StatsJSONPre121 is a backcompatibility struct along with ContainerConfig +type StatsJSONPre121 struct { + Stats + + // Network is for fallback stats where API Version < 1.21 + Network NetworkStats `json:"network,omitempty"` +} + +// StatsJSON is newly used Networks +type StatsJSON struct { + Stats + + // Networks request version >=1.21 + Networks map[string]NetworkStats `json:"networks,omitempty"` } diff --git a/daemon/stats.go b/daemon/stats.go index 9d8ec0cbf7..fd5809a89f 100644 --- a/daemon/stats.go +++ b/daemon/stats.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/daemon/execdriver" + "github.com/docker/docker/pkg/version" "github.com/docker/libnetwork/osl" "github.com/opencontainers/runc/libcontainer" ) @@ -16,6 +17,7 @@ type ContainerStatsConfig struct { Stream bool OutStream io.Writer Stop <-chan bool + Version version.Version } // ContainerStats writes information about the container to the stream @@ -31,7 +33,7 @@ func (daemon *Daemon) ContainerStats(container *Container, config *ContainerStat } var preCPUStats types.CPUStats - getStat := func(v interface{}) *types.Stats { + getStatJSON := func(v interface{}) *types.StatsJSON { update := v.(*execdriver.ResourceStats) // Retrieve the nw statistics from libnetwork and inject them in the Stats if nwStats, err := daemon.getNetworkStats(container); err == nil { @@ -58,14 +60,64 @@ func (daemon *Daemon) ContainerStats(container *Container, config *ContainerStat return nil } - s := getStat(v) + statsJSON := getStatJSON(v) + if config.Version.LessThan("1.21") { + var ( + rxBytes uint64 + rxPackets uint64 + rxErrors uint64 + rxDropped uint64 + txBytes uint64 + txPackets uint64 + txErrors uint64 + txDropped uint64 + ) + for _, v := range statsJSON.Networks { + rxBytes += v.RxBytes + rxPackets += v.RxPackets + rxErrors += v.RxErrors + rxDropped += v.RxDropped + txBytes += v.TxBytes + txPackets += v.TxPackets + txErrors += v.TxErrors + txDropped += v.TxDropped + } + statsJSONPre121 := &types.StatsJSONPre121{ + Stats: statsJSON.Stats, + Network: types.NetworkStats{ + RxBytes: rxBytes, + RxPackets: rxPackets, + RxErrors: rxErrors, + RxDropped: rxDropped, + TxBytes: txBytes, + TxPackets: txPackets, + TxErrors: txErrors, + TxDropped: txDropped, + }, + } + + if !config.Stream && noStreamFirstFrame { + // prime the cpu stats so they aren't 0 in the final output + noStreamFirstFrame = false + continue + } + + if err := enc.Encode(statsJSONPre121); err != nil { + return err + } + + if !config.Stream { + return nil + } + } + if !config.Stream && noStreamFirstFrame { // prime the cpu stats so they aren't 0 in the final output noStreamFirstFrame = false continue } - if err := enc.Encode(s); err != nil { + if err := enc.Encode(statsJSON); err != nil { return err } diff --git a/daemon/stats_freebsd.go b/daemon/stats_freebsd.go index 4ec7c657ce..1898ca9d40 100644 --- a/daemon/stats_freebsd.go +++ b/daemon/stats_freebsd.go @@ -6,9 +6,9 @@ import ( ) // convertStatsToAPITypes converts the libcontainer.Stats to the api specific -// structs. This is done to preserve API compatibility and versioning. -func convertStatsToAPITypes(ls *libcontainer.Stats) *types.Stats { +// structs. This is done to preserve API compatibility and versioning. +func convertStatsToAPITypes(ls *libcontainer.Stats) *types.StatsJSON { // TODO FreeBSD. Refactor accordingly to fill in stats. - s := &types.Stats{} + s := &types.StatsJSON{} return s } diff --git a/daemon/stats_linux.go b/daemon/stats_linux.go index 513106975a..466f2df5e7 100644 --- a/daemon/stats_linux.go +++ b/daemon/stats_linux.go @@ -7,20 +7,24 @@ import ( ) // convertStatsToAPITypes converts the libcontainer.Stats to the api specific -// structs. This is done to preserve API compatibility and versioning. -func convertStatsToAPITypes(ls *libcontainer.Stats) *types.Stats { - s := &types.Stats{} +// structs. This is done to preserve API compatibility and versioning. +func convertStatsToAPITypes(ls *libcontainer.Stats) *types.StatsJSON { + s := &types.StatsJSON{} if ls.Interfaces != nil { - s.Network = types.NetworkStats{} + s.Networks = make(map[string]types.NetworkStats) for _, iface := range ls.Interfaces { - s.Network.RxBytes += iface.RxBytes - s.Network.RxPackets += iface.RxPackets - s.Network.RxErrors += iface.RxErrors - s.Network.RxDropped += iface.RxDropped - s.Network.TxBytes += iface.TxBytes - s.Network.TxPackets += iface.TxPackets - s.Network.TxErrors += iface.TxErrors - s.Network.TxDropped += iface.TxDropped + // For API Version >= 1.21, the original data of network will + // be returned. + s.Networks[iface.Name] = types.NetworkStats{ + RxBytes: iface.RxBytes, + RxPackets: iface.RxPackets, + RxErrors: iface.RxErrors, + RxDropped: iface.RxDropped, + TxBytes: iface.TxBytes, + TxPackets: iface.TxPackets, + TxErrors: iface.TxErrors, + TxDropped: iface.TxDropped, + } } } diff --git a/daemon/stats_windows.go b/daemon/stats_windows.go index c79eb6401d..fc8991ba2a 100644 --- a/daemon/stats_windows.go +++ b/daemon/stats_windows.go @@ -6,9 +6,9 @@ import ( ) // convertStatsToAPITypes converts the libcontainer.Stats to the api specific -// structs. This is done to preserve API compatibility and versioning. -func convertStatsToAPITypes(ls *libcontainer.Stats) *types.Stats { +// structs. This is done to preserve API compatibility and versioning. +func convertStatsToAPITypes(ls *libcontainer.Stats) *types.StatsJSON { // TODO Windows. Refactor accordingly to fill in stats. - s := &types.Stats{} + s := &types.StatsJSON{} return s } diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 0d09f5c4ef..743d8650b4 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -83,6 +83,7 @@ This section lists each version from latest to oldest. Each listing includes a * `VolumeDriver` has been moved from config to hostConfig to make the configuration portable. * `GET /images/(name)/json` now returns information about tags of the image. * The `config` option now accepts the field `StopSignal`, which specifies the signal to use to kill a container. +* `GET /containers/(id)/stats` will return networking information respectively for each interface. ### v1.20 API changes diff --git a/docs/reference/api/docker_remote_api_v1.21.md b/docs/reference/api/docker_remote_api_v1.21.md index 9d307b93f2..45cf220443 100644 --- a/docs/reference/api/docker_remote_api_v1.21.md +++ b/docs/reference/api/docker_remote_api_v1.21.md @@ -631,15 +631,27 @@ This endpoint returns a live stream of a container's resource usage statistics. { "read" : "2015-01-08T22:57:31.547920715Z", - "network" : { - "rx_dropped" : 0, - "rx_bytes" : 648, - "rx_errors" : 0, - "tx_packets" : 8, - "tx_dropped" : 0, - "rx_packets" : 8, - "tx_errors" : 0, - "tx_bytes" : 648 + "network": { + "eth0": { + "rx_bytes": 5338, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 36, + "tx_bytes": 648, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 8 + }, + "eth5": { + "rx_bytes": 4641, + "rx_dropped": 0, + "rx_errors": 0, + "rx_packets": 26, + "tx_bytes": 690, + "tx_dropped": 0, + "tx_errors": 0, + "tx_packets": 9 + } }, "memory_stats" : { "stats" : { diff --git a/integration-cli/docker_api_stats_test.go b/integration-cli/docker_api_stats_test.go index abaa7c953d..a13d4c7321 100644 --- a/integration-cli/docker_api_stats_test.go +++ b/integration-cli/docker_api_stats_test.go @@ -87,8 +87,18 @@ func (s *DockerSuite) TestApiNetworkStats(c *check.C) { contIP := findContainerIP(c, id) numPings := 10 + var preRxPackets uint64 + var preTxPackets uint64 + var postRxPackets uint64 + var postTxPackets uint64 + // Get the container networking stats before and after pinging the container nwStatsPre := getNetworkStats(c, id) + for _, v := range nwStatsPre { + preRxPackets += v.RxPackets + preTxPackets += v.TxPackets + } + countParam := "-c" if runtime.GOOS == "windows" { countParam = "-n" // Ping count parameter is -n on Windows @@ -97,18 +107,22 @@ func (s *DockerSuite) TestApiNetworkStats(c *check.C) { pingouts := string(pingout[:]) c.Assert(err, check.IsNil) nwStatsPost := getNetworkStats(c, id) + for _, v := range nwStatsPost { + postRxPackets += v.RxPackets + postTxPackets += v.TxPackets + } // Verify the stats contain at least the expected number of packets (account for ARP) - expRxPkts := 1 + nwStatsPre.RxPackets + uint64(numPings) - expTxPkts := 1 + nwStatsPre.TxPackets + uint64(numPings) - c.Assert(nwStatsPost.TxPackets >= expTxPkts, check.Equals, true, - check.Commentf("Reported less TxPackets than expected. Expected >= %d. Found %d. %s", expTxPkts, nwStatsPost.TxPackets, pingouts)) - c.Assert(nwStatsPost.RxPackets >= expRxPkts, check.Equals, true, - check.Commentf("Reported less Txbytes than expected. Expected >= %d. Found %d. %s", expRxPkts, nwStatsPost.RxPackets, pingouts)) + expRxPkts := 1 + preRxPackets + uint64(numPings) + expTxPkts := 1 + preTxPackets + uint64(numPings) + c.Assert(postTxPackets >= expTxPkts, check.Equals, true, + check.Commentf("Reported less TxPackets than expected. Expected >= %d. Found %d. %s", expTxPkts, postTxPackets, pingouts)) + c.Assert(postRxPackets >= expRxPkts, check.Equals, true, + check.Commentf("Reported less Txbytes than expected. Expected >= %d. Found %d. %s", expRxPkts, postRxPackets, pingouts)) } -func getNetworkStats(c *check.C, id string) types.NetworkStats { - var st *types.Stats +func getNetworkStats(c *check.C, id string) map[string]types.NetworkStats { + var st *types.StatsJSON _, body, err := sockRequestRaw("GET", fmt.Sprintf("/containers/%s/stats?stream=false", id), nil, "") c.Assert(err, check.IsNil) @@ -117,5 +131,5 @@ func getNetworkStats(c *check.C, id string) types.NetworkStats { c.Assert(err, check.IsNil) body.Close() - return st.Network + return st.Networks }