github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/integration-cli/docker_api_stats_test.go (about) 1 package main 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "os/exec" 9 "runtime" 10 "strconv" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/docker/docker/api/types" 17 "github.com/docker/docker/api/types/versions" 18 "github.com/docker/docker/client" 19 "github.com/docker/docker/testutil/request" 20 "gotest.tools/v3/assert" 21 ) 22 23 var expectedNetworkInterfaceStats = strings.Split("rx_bytes rx_dropped rx_errors rx_packets tx_bytes tx_dropped tx_errors tx_packets", " ") 24 25 func (s *DockerSuite) TestAPIStatsNoStreamGetCpu(c *testing.T) { 26 out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "while true;usleep 100; do echo 'Hello'; done") 27 28 id := strings.TrimSpace(out) 29 assert.NilError(c, waitRun(id)) 30 resp, body, err := request.Get(fmt.Sprintf("/containers/%s/stats?stream=false", id)) 31 assert.NilError(c, err) 32 assert.Equal(c, resp.StatusCode, http.StatusOK) 33 assert.Equal(c, resp.Header.Get("Content-Type"), "application/json") 34 assert.Equal(c, resp.Header.Get("Content-Type"), "application/json") 35 36 var v *types.Stats 37 err = json.NewDecoder(body).Decode(&v) 38 assert.NilError(c, err) 39 body.Close() 40 41 var cpuPercent = 0.0 42 43 if testEnv.OSType != "windows" { 44 cpuDelta := float64(v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage) 45 systemDelta := float64(v.CPUStats.SystemUsage - v.PreCPUStats.SystemUsage) 46 cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 47 } else { 48 // Max number of 100ns intervals between the previous time read and now 49 possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals 50 possIntervals /= 100 // Convert to number of 100ns intervals 51 possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors 52 53 // Intervals used 54 intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage 55 56 // Percentage avoiding divide-by-zero 57 if possIntervals > 0 { 58 cpuPercent = float64(intervalsUsed) / float64(possIntervals) * 100.0 59 } 60 } 61 62 assert.Assert(c, cpuPercent != 0.0, "docker stats with no-stream get cpu usage failed: was %v", cpuPercent) 63 } 64 65 func (s *DockerSuite) TestAPIStatsStoppedContainerInGoroutines(c *testing.T) { 66 out, _ := dockerCmd(c, "run", "-d", "busybox", "/bin/sh", "-c", "echo 1") 67 id := strings.TrimSpace(out) 68 69 getGoRoutines := func() int { 70 _, body, err := request.Get(fmt.Sprintf("/info")) 71 assert.NilError(c, err) 72 info := types.Info{} 73 err = json.NewDecoder(body).Decode(&info) 74 assert.NilError(c, err) 75 body.Close() 76 return info.NGoroutines 77 } 78 79 // When the HTTP connection is closed, the number of goroutines should not increase. 80 routines := getGoRoutines() 81 _, body, err := request.Get(fmt.Sprintf("/containers/%s/stats", id)) 82 assert.NilError(c, err) 83 body.Close() 84 85 t := time.After(30 * time.Second) 86 for { 87 select { 88 case <-t: 89 assert.Assert(c, getGoRoutines() <= routines) 90 return 91 default: 92 if n := getGoRoutines(); n <= routines { 93 return 94 } 95 time.Sleep(200 * time.Millisecond) 96 } 97 } 98 } 99 100 func (s *DockerSuite) TestAPIStatsNetworkStats(c *testing.T) { 101 testRequires(c, testEnv.IsLocalDaemon) 102 103 out := runSleepingContainer(c) 104 id := strings.TrimSpace(out) 105 assert.NilError(c, waitRun(id)) 106 107 // Retrieve the container address 108 net := "bridge" 109 if testEnv.OSType == "windows" { 110 net = "nat" 111 } 112 contIP := findContainerIP(c, id, net) 113 numPings := 1 114 115 var preRxPackets uint64 116 var preTxPackets uint64 117 var postRxPackets uint64 118 var postTxPackets uint64 119 120 // Get the container networking stats before and after pinging the container 121 nwStatsPre := getNetworkStats(c, id) 122 for _, v := range nwStatsPre { 123 preRxPackets += v.RxPackets 124 preTxPackets += v.TxPackets 125 } 126 127 countParam := "-c" 128 if runtime.GOOS == "windows" { 129 countParam = "-n" // Ping count parameter is -n on Windows 130 } 131 pingout, err := exec.Command("ping", contIP, countParam, strconv.Itoa(numPings)).CombinedOutput() 132 if err != nil && runtime.GOOS == "linux" { 133 // If it fails then try a work-around, but just for linux. 134 // If this fails too then go back to the old error for reporting. 135 // 136 // The ping will sometimes fail due to an apparmor issue where it 137 // denies access to the libc.so.6 shared library - running it 138 // via /lib64/ld-linux-x86-64.so.2 seems to work around it. 139 pingout2, err2 := exec.Command("/lib64/ld-linux-x86-64.so.2", "/bin/ping", contIP, "-c", strconv.Itoa(numPings)).CombinedOutput() 140 if err2 == nil { 141 pingout = pingout2 142 err = err2 143 } 144 } 145 assert.NilError(c, err) 146 pingouts := string(pingout[:]) 147 nwStatsPost := getNetworkStats(c, id) 148 for _, v := range nwStatsPost { 149 postRxPackets += v.RxPackets 150 postTxPackets += v.TxPackets 151 } 152 153 // Verify the stats contain at least the expected number of packets 154 // On Linux, account for ARP. 155 expRxPkts := preRxPackets + uint64(numPings) 156 expTxPkts := preTxPackets + uint64(numPings) 157 if testEnv.OSType != "windows" { 158 expRxPkts++ 159 expTxPkts++ 160 } 161 assert.Assert(c, postTxPackets >= expTxPkts, "Reported less TxPackets than expected. Expected >= %d. Found %d. %s", expTxPkts, postTxPackets, pingouts) 162 assert.Assert(c, postRxPackets >= expRxPkts, "Reported less RxPackets than expected. Expected >= %d. Found %d. %s", expRxPkts, postRxPackets, pingouts) 163 } 164 165 func (s *DockerSuite) TestAPIStatsNetworkStatsVersioning(c *testing.T) { 166 // Windows doesn't support API versions less than 1.25, so no point testing 1.17 .. 1.21 167 testRequires(c, testEnv.IsLocalDaemon, DaemonIsLinux) 168 169 out := runSleepingContainer(c) 170 id := strings.TrimSpace(out) 171 assert.NilError(c, waitRun(id)) 172 wg := sync.WaitGroup{} 173 174 for i := 17; i <= 21; i++ { 175 wg.Add(1) 176 go func(i int) { 177 defer wg.Done() 178 apiVersion := fmt.Sprintf("v1.%d", i) 179 statsJSONBlob := getVersionedStats(c, id, apiVersion) 180 if versions.LessThan(apiVersion, "v1.21") { 181 assert.Assert(c, jsonBlobHasLTv121NetworkStats(statsJSONBlob), "Stats JSON blob from API %s %#v does not look like a <v1.21 API stats structure", apiVersion, statsJSONBlob) 182 } else { 183 assert.Assert(c, jsonBlobHasGTE121NetworkStats(statsJSONBlob), "Stats JSON blob from API %s %#v does not look like a >=v1.21 API stats structure", apiVersion, statsJSONBlob) 184 } 185 }(i) 186 } 187 wg.Wait() 188 } 189 190 func getNetworkStats(c *testing.T, id string) map[string]types.NetworkStats { 191 var st *types.StatsJSON 192 193 _, body, err := request.Get(fmt.Sprintf("/containers/%s/stats?stream=false", id)) 194 assert.NilError(c, err) 195 196 err = json.NewDecoder(body).Decode(&st) 197 assert.NilError(c, err) 198 body.Close() 199 200 return st.Networks 201 } 202 203 // getVersionedStats returns stats result for the 204 // container with id using an API call with version apiVersion. Since the 205 // stats result type differs between API versions, we simply return 206 // map[string]interface{}. 207 func getVersionedStats(c *testing.T, id string, apiVersion string) map[string]interface{} { 208 stats := make(map[string]interface{}) 209 210 _, body, err := request.Get(fmt.Sprintf("/%s/containers/%s/stats?stream=false", apiVersion, id)) 211 assert.NilError(c, err) 212 defer body.Close() 213 214 err = json.NewDecoder(body).Decode(&stats) 215 assert.NilError(c, err, "failed to decode stat: %s", err) 216 217 return stats 218 } 219 220 func jsonBlobHasLTv121NetworkStats(blob map[string]interface{}) bool { 221 networkStatsIntfc, ok := blob["network"] 222 if !ok { 223 return false 224 } 225 networkStats, ok := networkStatsIntfc.(map[string]interface{}) 226 if !ok { 227 return false 228 } 229 for _, expectedKey := range expectedNetworkInterfaceStats { 230 if _, ok := networkStats[expectedKey]; !ok { 231 return false 232 } 233 } 234 return true 235 } 236 237 func jsonBlobHasGTE121NetworkStats(blob map[string]interface{}) bool { 238 networksStatsIntfc, ok := blob["networks"] 239 if !ok { 240 return false 241 } 242 networksStats, ok := networksStatsIntfc.(map[string]interface{}) 243 if !ok { 244 return false 245 } 246 for _, networkInterfaceStatsIntfc := range networksStats { 247 networkInterfaceStats, ok := networkInterfaceStatsIntfc.(map[string]interface{}) 248 if !ok { 249 return false 250 } 251 for _, expectedKey := range expectedNetworkInterfaceStats { 252 if _, ok := networkInterfaceStats[expectedKey]; !ok { 253 return false 254 } 255 } 256 } 257 return true 258 } 259 260 func (s *DockerSuite) TestAPIStatsContainerNotFound(c *testing.T) { 261 testRequires(c, DaemonIsLinux) 262 cli, err := client.NewClientWithOpts(client.FromEnv) 263 assert.NilError(c, err) 264 defer cli.Close() 265 266 expected := "No such container: nonexistent" 267 268 _, err = cli.ContainerStats(context.Background(), "nonexistent", true) 269 assert.ErrorContains(c, err, expected) 270 _, err = cli.ContainerStats(context.Background(), "nonexistent", false) 271 assert.ErrorContains(c, err, expected) 272 } 273 274 func (s *DockerSuite) TestAPIStatsNoStreamConnectedContainers(c *testing.T) { 275 testRequires(c, DaemonIsLinux) 276 277 out1 := runSleepingContainer(c) 278 id1 := strings.TrimSpace(out1) 279 assert.NilError(c, waitRun(id1)) 280 281 out2 := runSleepingContainer(c, "--net", "container:"+id1) 282 id2 := strings.TrimSpace(out2) 283 assert.NilError(c, waitRun(id2)) 284 285 ch := make(chan error, 1) 286 go func() { 287 resp, body, err := request.Get(fmt.Sprintf("/containers/%s/stats?stream=false", id2)) 288 defer body.Close() 289 if err != nil { 290 ch <- err 291 } 292 if resp.StatusCode != http.StatusOK { 293 ch <- fmt.Errorf("Invalid StatusCode %v", resp.StatusCode) 294 } 295 if resp.Header.Get("Content-Type") != "application/json" { 296 ch <- fmt.Errorf("Invalid 'Content-Type' %v", resp.Header.Get("Content-Type")) 297 } 298 var v *types.Stats 299 if err := json.NewDecoder(body).Decode(&v); err != nil { 300 ch <- err 301 } 302 ch <- nil 303 }() 304 305 select { 306 case err := <-ch: 307 assert.NilError(c, err, "Error in stats Engine API: %v", err) 308 case <-time.After(15 * time.Second): 309 c.Fatalf("Stats did not return after timeout") 310 } 311 }