Add stats options to not prime the stats
| ... | ... |
@@ -105,9 +105,14 @@ func (s *containerRouter) getContainersStats(ctx context.Context, w http.Respons |
| 105 | 105 |
if !stream {
|
| 106 | 106 |
w.Header().Set("Content-Type", "application/json")
|
| 107 | 107 |
} |
| 108 |
+ var oneShot bool |
|
| 109 |
+ if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.41") {
|
|
| 110 |
+ oneShot = httputils.BoolValueOrDefault(r, "one-shot", false) |
|
| 111 |
+ } |
|
| 108 | 112 |
|
| 109 | 113 |
config := &backend.ContainerStatsConfig{
|
| 110 | 114 |
Stream: stream, |
| 115 |
+ OneShot: oneShot, |
|
| 111 | 116 |
OutStream: w, |
| 112 | 117 |
Version: httputils.VersionFromContext(ctx), |
| 113 | 118 |
} |
| ... | ... |
@@ -5691,6 +5691,11 @@ paths: |
| 5691 | 5691 |
description: "Stream the output. If false, the stats will be output once and then it will disconnect." |
| 5692 | 5692 |
type: "boolean" |
| 5693 | 5693 |
default: true |
| 5694 |
+ - name: "one-shot" |
|
| 5695 |
+ in: "query" |
|
| 5696 |
+ description: "Only get a single stat instead of waiting for 2 cycles. Must be used with stream=false" |
|
| 5697 |
+ type: "boolean" |
|
| 5698 |
+ default: false |
|
| 5694 | 5699 |
tags: ["Container"] |
| 5695 | 5700 |
/containers/{id}/resize:
|
| 5696 | 5701 |
post: |
| ... | ... |
@@ -24,3 +24,19 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea |
| 24 | 24 |
osType := getDockerOS(resp.header.Get("Server"))
|
| 25 | 25 |
return types.ContainerStats{Body: resp.body, OSType: osType}, err
|
| 26 | 26 |
} |
| 27 |
+ |
|
| 28 |
+// ContainerStatsOneShot gets a single stat entry from a container. |
|
| 29 |
+// It differs from `ContainerStats` in that the API should not wait to prime the stats |
|
| 30 |
+func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string) (types.ContainerStats, error) {
|
|
| 31 |
+ query := url.Values{}
|
|
| 32 |
+ query.Set("stream", "0")
|
|
| 33 |
+ query.Set("one-shot", "1")
|
|
| 34 |
+ |
|
| 35 |
+ resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) |
|
| 36 |
+ if err != nil {
|
|
| 37 |
+ return types.ContainerStats{}, err
|
|
| 38 |
+ } |
|
| 39 |
+ |
|
| 40 |
+ osType := getDockerOS(resp.header.Get("Server"))
|
|
| 41 |
+ return types.ContainerStats{Body: resp.body, OSType: osType}, err
|
|
| 42 |
+} |
| ... | ... |
@@ -67,6 +67,7 @@ type ContainerAPIClient interface {
|
| 67 | 67 |
ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error |
| 68 | 68 |
ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) |
| 69 | 69 |
ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error) |
| 70 |
+ ContainerStatsOneShot(ctx context.Context, container string) (types.ContainerStats, error) |
|
| 70 | 71 |
ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error |
| 71 | 72 |
ContainerStop(ctx context.Context, container string, timeout *time.Duration) error |
| 72 | 73 |
ContainerTop(ctx context.Context, container string, arguments []string) (containertypes.ContainerTopOKBody, error) |
| ... | ... |
@@ -12,6 +12,7 @@ import ( |
| 12 | 12 |
"github.com/docker/docker/api/types/versions" |
| 13 | 13 |
"github.com/docker/docker/api/types/versions/v1p20" |
| 14 | 14 |
"github.com/docker/docker/container" |
| 15 |
+ "github.com/docker/docker/errdefs" |
|
| 15 | 16 |
"github.com/docker/docker/pkg/ioutils" |
| 16 | 17 |
) |
| 17 | 18 |
|
| ... | ... |
@@ -30,6 +31,10 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c |
| 30 | 30 |
return err |
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 |
+ if config.Stream && config.OneShot {
|
|
| 34 |
+ return errdefs.InvalidParameter(errors.New("cannot have stream=true and one-shot=true"))
|
|
| 35 |
+ } |
|
| 36 |
+ |
|
| 33 | 37 |
// If the container is either not running or restarting and requires no stream, return an empty stats. |
| 34 | 38 |
if (!ctr.IsRunning() || ctr.IsRestarting()) && !config.Stream {
|
| 35 | 39 |
return json.NewEncoder(config.OutStream).Encode(&types.StatsJSON{
|
| ... | ... |
@@ -64,7 +69,7 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c |
| 64 | 64 |
updates := daemon.subscribeToContainerStats(ctr) |
| 65 | 65 |
defer daemon.unsubscribeToContainerStats(ctr, updates) |
| 66 | 66 |
|
| 67 |
- noStreamFirstFrame := true |
|
| 67 |
+ noStreamFirstFrame := !config.OneShot |
|
| 68 | 68 |
for {
|
| 69 | 69 |
select {
|
| 70 | 70 |
case v, ok := <-updates: |
| ... | ... |
@@ -61,6 +61,8 @@ keywords: "API, Docker, rcli, REST, documentation" |
| 61 | 61 |
service. |
| 62 | 62 |
* `GET /tasks/{id}` now includes `JobIteration` on the task if spawned from a
|
| 63 | 63 |
job-mode service. |
| 64 |
+* `GET /containers/{id}/stats` now accepts a query param (`one-shot`) which, when used with `stream=false` fetches a
|
|
| 65 |
+ single set of stats instead of waiting for two collection cycles to have 2 CPU stats over a 1 second period. |
|
| 64 | 66 |
|
| 65 | 67 |
## v1.40 API changes |
| 66 | 68 |
|
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"context" |
| 5 | 5 |
"encoding/json" |
| 6 | 6 |
"io" |
| 7 |
+ "reflect" |
|
| 7 | 8 |
"testing" |
| 8 | 9 |
"time" |
| 9 | 10 |
|
| ... | ... |
@@ -34,10 +35,23 @@ func TestStats(t *testing.T) {
|
| 34 | 34 |
assert.NilError(t, err) |
| 35 | 35 |
defer resp.Body.Close() |
| 36 | 36 |
|
| 37 |
- var v *types.Stats |
|
| 37 |
+ var v types.Stats |
|
| 38 | 38 |
err = json.NewDecoder(resp.Body).Decode(&v) |
| 39 | 39 |
assert.NilError(t, err) |
| 40 | 40 |
assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal)) |
| 41 |
+ assert.Check(t, !reflect.DeepEqual(v.PreCPUStats, types.CPUStats{}))
|
|
| 42 |
+ err = json.NewDecoder(resp.Body).Decode(&v) |
|
| 43 |
+ assert.Assert(t, is.ErrorContains(err, ""), io.EOF) |
|
| 44 |
+ |
|
| 45 |
+ resp, err = client.ContainerStatsOneShot(ctx, cID) |
|
| 46 |
+ assert.NilError(t, err) |
|
| 47 |
+ defer resp.Body.Close() |
|
| 48 |
+ |
|
| 49 |
+ v = types.Stats{}
|
|
| 50 |
+ err = json.NewDecoder(resp.Body).Decode(&v) |
|
| 51 |
+ assert.NilError(t, err) |
|
| 52 |
+ assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal)) |
|
| 53 |
+ assert.Check(t, is.DeepEqual(v.PreCPUStats, types.CPUStats{}))
|
|
| 41 | 54 |
err = json.NewDecoder(resp.Body).Decode(&v) |
| 42 | 55 |
assert.Assert(t, is.ErrorContains(err, ""), io.EOF) |
| 43 | 56 |
} |