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