diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index b41e540ad0..c1db782db8 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -105,9 +105,14 @@ func (s *containerRouter) getContainersStats(ctx context.Context, w http.Respons if !stream { w.Header().Set("Content-Type", "application/json") } + var oneShot bool + if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.41") { + oneShot = httputils.BoolValueOrDefault(r, "one-shot", false) + } config := &backend.ContainerStatsConfig{ Stream: stream, + OneShot: oneShot, OutStream: w, Version: httputils.VersionFromContext(ctx), } diff --git a/api/swagger.yaml b/api/swagger.yaml index 6933fb1675..2ae9afea29 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -5685,6 +5685,11 @@ paths: description: "Stream the output. If false, the stats will be output once and then it will disconnect." type: "boolean" default: true + - name: "one-shot" + in: "query" + description: "Only get a single stat instead of waiting for 2 cycles. Must be used with stream=false" + type: "boolean" + default: false tags: ["Container"] /containers/{id}/resize: post: diff --git a/api/types/backend/backend.go b/api/types/backend/backend.go index 6afdd16dfc..9880c632bd 100644 --- a/api/types/backend/backend.go +++ b/api/types/backend/backend.go @@ -73,6 +73,7 @@ type LogSelector struct { // behavior of a backend.ContainerStats() call. type ContainerStatsConfig struct { Stream bool + OneShot bool OutStream io.Writer Version string } diff --git a/client/container_stats.go b/client/container_stats.go index 6ef44c7748..0a6488dde8 100644 --- a/client/container_stats.go +++ b/client/container_stats.go @@ -24,3 +24,19 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea osType := getDockerOS(resp.header.Get("Server")) return types.ContainerStats{Body: resp.body, OSType: osType}, err } + +// ContainerStatsOneShot gets a single stat entry from a container. +// It differs from `ContainerStats` in that the API should not wait to prime the stats +func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string) (types.ContainerStats, error) { + query := url.Values{} + query.Set("stream", "0") + query.Set("one-shot", "1") + + resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) + if err != nil { + return types.ContainerStats{}, err + } + + osType := getDockerOS(resp.header.Get("Server")) + return types.ContainerStats{Body: resp.body, OSType: osType}, err +} diff --git a/client/interface.go b/client/interface.go index cde64be4b5..4f9fd67354 100644 --- a/client/interface.go +++ b/client/interface.go @@ -67,6 +67,7 @@ type ContainerAPIClient interface { ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error) + ContainerStatsOneShot(ctx context.Context, container string) (types.ContainerStats, error) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error ContainerStop(ctx context.Context, container string, timeout *time.Duration) error ContainerTop(ctx context.Context, container string, arguments []string) (containertypes.ContainerTopOKBody, error) diff --git a/daemon/stats.go b/daemon/stats.go index 006d2223b2..97910dab0b 100644 --- a/daemon/stats.go +++ b/daemon/stats.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions/v1p20" "github.com/docker/docker/container" + "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/ioutils" ) @@ -30,6 +31,10 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c return err } + if config.Stream && config.OneShot { + return errdefs.InvalidParameter(errors.New("cannot have stream=true and one-shot=true")) + } + // If the container is either not running or restarting and requires no stream, return an empty stats. if (!container.IsRunning() || container.IsRestarting()) && !config.Stream { return json.NewEncoder(config.OutStream).Encode(&types.StatsJSON{ @@ -63,7 +68,7 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c updates := daemon.subscribeToContainerStats(container) defer daemon.unsubscribeToContainerStats(container, updates) - noStreamFirstFrame := true + noStreamFirstFrame := !config.OneShot for { select { case v, ok := <-updates: diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 8366ac866f..8a11e4d0e2 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -57,6 +57,8 @@ keywords: "API, Docker, rcli, REST, documentation" service. * `GET /tasks/{id}` now includes `JobIteration` on the task if spawned from a job-mode service. +* `GET /containers/{id}/stats` now accepts a query param (`one-shot`) which, when used with `stream=false` fetches a + single set of stats instead of waiting for two collection cycles to have 2 CPU stats over a 1 second period. ## v1.40 API changes diff --git a/integration/container/stats_test.go b/integration/container/stats_test.go index a33f8f809c..b4b7aa3b34 100644 --- a/integration/container/stats_test.go +++ b/integration/container/stats_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "io" + "reflect" "testing" "time" @@ -33,10 +34,23 @@ func TestStats(t *testing.T) { assert.NilError(t, err) defer resp.Body.Close() - var v *types.Stats + var v types.Stats err = json.NewDecoder(resp.Body).Decode(&v) assert.NilError(t, err) assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal)) + assert.Check(t, !reflect.DeepEqual(v.PreCPUStats, types.CPUStats{})) + err = json.NewDecoder(resp.Body).Decode(&v) + assert.Assert(t, is.ErrorContains(err, ""), io.EOF) + + resp, err = client.ContainerStatsOneShot(ctx, cID) + assert.NilError(t, err) + defer resp.Body.Close() + + v = types.Stats{} + err = json.NewDecoder(resp.Body).Decode(&v) + assert.NilError(t, err) + assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal)) + assert.Check(t, is.DeepEqual(v.PreCPUStats, types.CPUStats{})) err = json.NewDecoder(resp.Body).Decode(&v) assert.Assert(t, is.ErrorContains(err, ""), io.EOF) }