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