mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
Merge pull request #51293 from thaJeztah/merge_stats
client: ContainerStats: add option, output-structs, remove ContainerStatsOneShot
This commit is contained in:
@@ -73,8 +73,7 @@ type ContainerAPIClient interface {
|
||||
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) error
|
||||
ContainerRestart(ctx context.Context, container string, options ContainerStopOptions) error
|
||||
ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error)
|
||||
ContainerStats(ctx context.Context, container string, stream bool) (StatsResponseReader, error)
|
||||
ContainerStatsOneShot(ctx context.Context, container string) (StatsResponseReader, error)
|
||||
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
|
||||
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) error
|
||||
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) error
|
||||
ContainerTop(ctx context.Context, container string, arguments []string) (container.TopResponse, error)
|
||||
|
||||
@@ -6,61 +6,72 @@ import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// StatsResponseReader wraps an [io.ReadCloser] to read (a stream of) stats
|
||||
// for a container, as produced by the GET "/stats" endpoint.
|
||||
//
|
||||
// The OSType field is set to the server's platform to allow
|
||||
// platform-specific handling of the response.
|
||||
//
|
||||
// TODO(thaJeztah): remove this wrapper, and make OSType part of [github.com/moby/moby/api/types/container.StatsResponse].
|
||||
type StatsResponseReader struct {
|
||||
Body io.ReadCloser `json:"body"`
|
||||
OSType string `json:"ostype"`
|
||||
// ContainerStatsOptions holds parameters to retrieve container statistics
|
||||
// using the [Client.ContainerStats] method.
|
||||
type ContainerStatsOptions struct {
|
||||
// Stream enables streaming [container.StatsResponse] results instead
|
||||
// of collecting a single sample. If enabled, the client remains attached
|
||||
// until the [ContainerStatsResult.Body] is closed or the context is
|
||||
// cancelled.
|
||||
Stream bool
|
||||
|
||||
// IncludePreviousSample asks the daemon to collect a prior sample to populate the
|
||||
// [container.StatsResponse.PreRead] and [container.StatsResponse.PreCPUStats]
|
||||
// fields.
|
||||
//
|
||||
// It set, the daemon collects two samples at a one-second interval before
|
||||
// returning the result. The first sample populates the PreCPUStats (“previous
|
||||
// CPU”) field, allowing delta calculations for CPU usage. If false, only
|
||||
// a single sample is taken and returned immediately, leaving PreRead and
|
||||
// PreCPUStats empty.
|
||||
//
|
||||
// This option has no effect if Stream is enabled. If Stream is enabled,
|
||||
// [container.StatsResponse.PreCPUStats] is never populated for the first
|
||||
// record.
|
||||
IncludePreviousSample bool
|
||||
}
|
||||
|
||||
// ContainerStats returns near realtime stats for a given container.
|
||||
// It's up to the caller to close the [io.ReadCloser] returned.
|
||||
func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (StatsResponseReader, error) {
|
||||
// ContainerStatsResult holds the result from [Client.ContainerStats].
|
||||
//
|
||||
// It wraps an [io.ReadCloser] that provides one or more [container.StatsResponse]
|
||||
// objects for a container, as produced by the "GET /containers/{id}/stats" endpoint.
|
||||
// If streaming is disabled, the stream contains a single record.
|
||||
//
|
||||
// The OSType field reports the daemon's operating system, allowing platform-specific
|
||||
// handling of the response.
|
||||
type ContainerStatsResult struct {
|
||||
Body io.ReadCloser
|
||||
OSType string // TODO(thaJeztah): consider moving OSType into [container.StatsResponse].
|
||||
}
|
||||
|
||||
// ContainerStats retrieves live resource usage statistics for the specified
|
||||
// container. The caller must close the [io.ReadCloser] in the returned result
|
||||
// to release associated resources.
|
||||
func (cli *Client) ContainerStats(ctx context.Context, containerID string, options ContainerStatsOptions) (ContainerStatsResult, error) {
|
||||
containerID, err := trimID("container", containerID)
|
||||
if err != nil {
|
||||
return StatsResponseReader{}, err
|
||||
return ContainerStatsResult{}, err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("stream", "0")
|
||||
if stream {
|
||||
query.Set("stream", "1")
|
||||
if options.Stream {
|
||||
query.Set("stream", "true")
|
||||
} else {
|
||||
// Note: daemons before v29.0 return an error if both set: "cannot have stream=true and one-shot=true"
|
||||
//
|
||||
// TODO(thaJeztah): consider making "stream=false" the default for the API as well, or using Accept Header to switch.
|
||||
query.Set("stream", "false")
|
||||
if !options.IncludePreviousSample {
|
||||
query.Set("one-shot", "true")
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil)
|
||||
if err != nil {
|
||||
return StatsResponseReader{}, err
|
||||
return ContainerStatsResult{}, err
|
||||
}
|
||||
|
||||
return StatsResponseReader{
|
||||
Body: resp.Body,
|
||||
OSType: resp.Header.Get("Ostype"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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) (StatsResponseReader, error) {
|
||||
containerID, err := trimID("container", containerID)
|
||||
if err != nil {
|
||||
return StatsResponseReader{}, err
|
||||
}
|
||||
|
||||
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 StatsResponseReader{}, err
|
||||
}
|
||||
|
||||
return StatsResponseReader{
|
||||
return ContainerStatsResult{
|
||||
Body: resp.Body,
|
||||
OSType: resp.Header.Get("Ostype"),
|
||||
}, nil
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@@ -15,14 +16,14 @@ import (
|
||||
func TestContainerStatsError(t *testing.T) {
|
||||
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
|
||||
assert.NilError(t, err)
|
||||
_, err = client.ContainerStats(context.Background(), "nothing", false)
|
||||
_, err = client.ContainerStats(t.Context(), "nothing", ContainerStatsOptions{})
|
||||
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
|
||||
|
||||
_, err = client.ContainerStats(context.Background(), "", false)
|
||||
_, err = client.ContainerStats(t.Context(), "", ContainerStatsOptions{})
|
||||
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
|
||||
assert.Check(t, is.ErrorContains(err, "value is empty"))
|
||||
|
||||
_, err = client.ContainerStats(context.Background(), " ", false)
|
||||
_, err = client.ContainerStats(t.Context(), " ", ContainerStatsOptions{})
|
||||
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
|
||||
assert.Check(t, is.ErrorContains(err, "value is empty"))
|
||||
}
|
||||
@@ -34,11 +35,11 @@ func TestContainerStats(t *testing.T) {
|
||||
expectedStream string
|
||||
}{
|
||||
{
|
||||
expectedStream: "0",
|
||||
expectedStream: "false",
|
||||
},
|
||||
{
|
||||
stream: true,
|
||||
expectedStream: "1",
|
||||
expectedStream: "true",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
@@ -52,16 +53,22 @@ func TestContainerStats(t *testing.T) {
|
||||
if stream != tc.expectedStream {
|
||||
return nil, fmt.Errorf("stream not set in URL query properly. Expected '%s', got %s", tc.expectedStream, stream)
|
||||
}
|
||||
return mockResponse(http.StatusOK, nil, "response")(req)
|
||||
return mockJSONResponse(http.StatusOK, nil, container.StatsResponse{ID: "container_id"})(req)
|
||||
}))
|
||||
assert.NilError(t, err)
|
||||
resp, err := client.ContainerStats(context.Background(), "container_id", tc.stream)
|
||||
resp, err := client.ContainerStats(t.Context(), "container_id", ContainerStatsOptions{
|
||||
Stream: tc.stream,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = resp.Body.Close()
|
||||
})
|
||||
content, err := io.ReadAll(resp.Body)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(string(content), "response"))
|
||||
|
||||
var stats container.StatsResponse
|
||||
err = json.Unmarshal(content, &stats)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(stats.ID, "container_id"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func (s *DockerAPISuite) TestGetContainerStats(c *testing.T) {
|
||||
runSleepingContainer(c, "--name", name)
|
||||
|
||||
type b struct {
|
||||
stats client.StatsResponseReader
|
||||
stats client.ContainerStatsResult
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -158,7 +158,9 @@ func (s *DockerAPISuite) TestGetContainerStats(c *testing.T) {
|
||||
assert.NilError(c, err)
|
||||
defer apiClient.Close()
|
||||
|
||||
stats, err := apiClient.ContainerStats(testutil.GetContext(c), name, true)
|
||||
stats, err := apiClient.ContainerStats(testutil.GetContext(c), name, client.ContainerStatsOptions{
|
||||
Stream: true,
|
||||
})
|
||||
assert.NilError(c, err)
|
||||
bc <- b{stats, err}
|
||||
}()
|
||||
@@ -192,7 +194,9 @@ func (s *DockerAPISuite) TestGetContainerStatsRmRunning(c *testing.T) {
|
||||
assert.NilError(c, err)
|
||||
defer apiClient.Close()
|
||||
|
||||
stats, err := apiClient.ContainerStats(testutil.GetContext(c), id, true)
|
||||
stats, err := apiClient.ContainerStats(testutil.GetContext(c), id, client.ContainerStatsOptions{
|
||||
Stream: true,
|
||||
})
|
||||
assert.NilError(c, err)
|
||||
defer stats.Body.Close()
|
||||
|
||||
@@ -253,7 +257,7 @@ func (s *DockerAPISuite) TestGetContainerStatsStream(c *testing.T) {
|
||||
runSleepingContainer(c, "--name", name)
|
||||
|
||||
type b struct {
|
||||
stats client.StatsResponseReader
|
||||
stats client.ContainerStatsResult
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -263,7 +267,9 @@ func (s *DockerAPISuite) TestGetContainerStatsStream(c *testing.T) {
|
||||
assert.NilError(c, err)
|
||||
defer apiClient.Close()
|
||||
|
||||
stats, err := apiClient.ContainerStats(testutil.GetContext(c), name, true)
|
||||
stats, err := apiClient.ContainerStats(testutil.GetContext(c), name, client.ContainerStatsOptions{
|
||||
Stream: true,
|
||||
})
|
||||
assert.NilError(c, err)
|
||||
bc <- b{stats, err}
|
||||
}()
|
||||
@@ -302,7 +308,10 @@ func (s *DockerAPISuite) TestGetContainerStatsNoStream(c *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(testutil.GetContext(c), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
stats, err := apiClient.ContainerStats(ctx, cID, false)
|
||||
stats, err := apiClient.ContainerStats(ctx, cID, client.ContainerStatsOptions{
|
||||
Stream: false,
|
||||
IncludePreviousSample: true,
|
||||
})
|
||||
assert.NilError(c, err)
|
||||
defer func() { _ = stats.Body.Close() }()
|
||||
|
||||
@@ -327,7 +336,10 @@ func (s *DockerAPISuite) TestGetStoppedContainerStats(c *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(testutil.GetContext(c), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
stats, err := apiClient.ContainerStats(ctx, name, false)
|
||||
stats, err := apiClient.ContainerStats(ctx, name, client.ContainerStatsOptions{
|
||||
Stream: false,
|
||||
IncludePreviousSample: true,
|
||||
})
|
||||
assert.NilError(c, err)
|
||||
defer func() { _ = stats.Body.Close() }()
|
||||
|
||||
@@ -1133,7 +1145,10 @@ func (s *DockerAPISuite) TestContainerAPIStatsWithNetworkDisabled(c *testing.T)
|
||||
ctx, cancel := context.WithTimeout(testutil.GetContext(c), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
stats, err := apiClient.ContainerStats(ctx, name, false)
|
||||
stats, err := apiClient.ContainerStats(ctx, name, client.ContainerStatsOptions{
|
||||
Stream: false,
|
||||
IncludePreviousSample: true,
|
||||
})
|
||||
assert.NilError(c, err)
|
||||
defer func() { _ = stats.Body.Close() }()
|
||||
|
||||
|
||||
@@ -184,11 +184,16 @@ func (s *DockerAPISuite) TestAPIStatsContainerNotFound(c *testing.T) {
|
||||
assert.NilError(c, err)
|
||||
defer func() { _ = apiClient.Close() }()
|
||||
|
||||
_, err = apiClient.ContainerStats(testutil.GetContext(c), "no-such-container", true)
|
||||
_, err = apiClient.ContainerStats(testutil.GetContext(c), "no-such-container", client.ContainerStatsOptions{
|
||||
Stream: true,
|
||||
})
|
||||
assert.ErrorType(c, err, cerrdefs.IsNotFound)
|
||||
assert.ErrorContains(c, err, "no-such-container")
|
||||
|
||||
_, err = apiClient.ContainerStats(testutil.GetContext(c), "no-such-container", false)
|
||||
_, err = apiClient.ContainerStats(testutil.GetContext(c), "no-such-container", client.ContainerStatsOptions{
|
||||
Stream: false,
|
||||
IncludePreviousSample: true,
|
||||
})
|
||||
assert.ErrorType(c, err, cerrdefs.IsNotFound)
|
||||
assert.ErrorContains(c, err, "no-such-container")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
containertypes "github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/moby/v2/integration/internal/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@@ -25,7 +26,10 @@ func TestStats(t *testing.T) {
|
||||
|
||||
cID := container.Run(ctx, t, apiClient)
|
||||
t.Run("no-stream", func(t *testing.T) {
|
||||
resp, err := apiClient.ContainerStats(ctx, cID, false)
|
||||
resp, err := apiClient.ContainerStats(ctx, cID, client.ContainerStatsOptions{
|
||||
Stream: false,
|
||||
IncludePreviousSample: true,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
@@ -39,7 +43,10 @@ func TestStats(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("one-shot", func(t *testing.T) {
|
||||
resp, err := apiClient.ContainerStatsOneShot(ctx, cID)
|
||||
resp, err := apiClient.ContainerStats(ctx, cID, client.ContainerStatsOptions{
|
||||
Stream: false,
|
||||
IncludePreviousSample: false,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
|
||||
3
vendor/github.com/moby/moby/client/client_interfaces.go
generated
vendored
3
vendor/github.com/moby/moby/client/client_interfaces.go
generated
vendored
@@ -73,8 +73,7 @@ type ContainerAPIClient interface {
|
||||
ContainerResize(ctx context.Context, container string, options ContainerResizeOptions) error
|
||||
ContainerRestart(ctx context.Context, container string, options ContainerStopOptions) error
|
||||
ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error)
|
||||
ContainerStats(ctx context.Context, container string, stream bool) (StatsResponseReader, error)
|
||||
ContainerStatsOneShot(ctx context.Context, container string) (StatsResponseReader, error)
|
||||
ContainerStats(ctx context.Context, container string, options ContainerStatsOptions) (ContainerStatsResult, error)
|
||||
ContainerStart(ctx context.Context, container string, options ContainerStartOptions) error
|
||||
ContainerStop(ctx context.Context, container string, options ContainerStopOptions) error
|
||||
ContainerTop(ctx context.Context, container string, arguments []string) (container.TopResponse, error)
|
||||
|
||||
95
vendor/github.com/moby/moby/client/container_stats.go
generated
vendored
95
vendor/github.com/moby/moby/client/container_stats.go
generated
vendored
@@ -6,61 +6,72 @@ import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// StatsResponseReader wraps an [io.ReadCloser] to read (a stream of) stats
|
||||
// for a container, as produced by the GET "/stats" endpoint.
|
||||
//
|
||||
// The OSType field is set to the server's platform to allow
|
||||
// platform-specific handling of the response.
|
||||
//
|
||||
// TODO(thaJeztah): remove this wrapper, and make OSType part of [github.com/moby/moby/api/types/container.StatsResponse].
|
||||
type StatsResponseReader struct {
|
||||
Body io.ReadCloser `json:"body"`
|
||||
OSType string `json:"ostype"`
|
||||
// ContainerStatsOptions holds parameters to retrieve container statistics
|
||||
// using the [Client.ContainerStats] method.
|
||||
type ContainerStatsOptions struct {
|
||||
// Stream enables streaming [container.StatsResponse] results instead
|
||||
// of collecting a single sample. If enabled, the client remains attached
|
||||
// until the [ContainerStatsResult.Body] is closed or the context is
|
||||
// cancelled.
|
||||
Stream bool
|
||||
|
||||
// IncludePreviousSample asks the daemon to collect a prior sample to populate the
|
||||
// [container.StatsResponse.PreRead] and [container.StatsResponse.PreCPUStats]
|
||||
// fields.
|
||||
//
|
||||
// It set, the daemon collects two samples at a one-second interval before
|
||||
// returning the result. The first sample populates the PreCPUStats (“previous
|
||||
// CPU”) field, allowing delta calculations for CPU usage. If false, only
|
||||
// a single sample is taken and returned immediately, leaving PreRead and
|
||||
// PreCPUStats empty.
|
||||
//
|
||||
// This option has no effect if Stream is enabled. If Stream is enabled,
|
||||
// [container.StatsResponse.PreCPUStats] is never populated for the first
|
||||
// record.
|
||||
IncludePreviousSample bool
|
||||
}
|
||||
|
||||
// ContainerStats returns near realtime stats for a given container.
|
||||
// It's up to the caller to close the [io.ReadCloser] returned.
|
||||
func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (StatsResponseReader, error) {
|
||||
// ContainerStatsResult holds the result from [Client.ContainerStats].
|
||||
//
|
||||
// It wraps an [io.ReadCloser] that provides one or more [container.StatsResponse]
|
||||
// objects for a container, as produced by the "GET /containers/{id}/stats" endpoint.
|
||||
// If streaming is disabled, the stream contains a single record.
|
||||
//
|
||||
// The OSType field reports the daemon's operating system, allowing platform-specific
|
||||
// handling of the response.
|
||||
type ContainerStatsResult struct {
|
||||
Body io.ReadCloser
|
||||
OSType string // TODO(thaJeztah): consider moving OSType into [container.StatsResponse].
|
||||
}
|
||||
|
||||
// ContainerStats retrieves live resource usage statistics for the specified
|
||||
// container. The caller must close the [io.ReadCloser] in the returned result
|
||||
// to release associated resources.
|
||||
func (cli *Client) ContainerStats(ctx context.Context, containerID string, options ContainerStatsOptions) (ContainerStatsResult, error) {
|
||||
containerID, err := trimID("container", containerID)
|
||||
if err != nil {
|
||||
return StatsResponseReader{}, err
|
||||
return ContainerStatsResult{}, err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("stream", "0")
|
||||
if stream {
|
||||
query.Set("stream", "1")
|
||||
if options.Stream {
|
||||
query.Set("stream", "true")
|
||||
} else {
|
||||
// Note: daemons before v29.0 return an error if both set: "cannot have stream=true and one-shot=true"
|
||||
//
|
||||
// TODO(thaJeztah): consider making "stream=false" the default for the API as well, or using Accept Header to switch.
|
||||
query.Set("stream", "false")
|
||||
if !options.IncludePreviousSample {
|
||||
query.Set("one-shot", "true")
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil)
|
||||
if err != nil {
|
||||
return StatsResponseReader{}, err
|
||||
return ContainerStatsResult{}, err
|
||||
}
|
||||
|
||||
return StatsResponseReader{
|
||||
Body: resp.Body,
|
||||
OSType: resp.Header.Get("Ostype"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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) (StatsResponseReader, error) {
|
||||
containerID, err := trimID("container", containerID)
|
||||
if err != nil {
|
||||
return StatsResponseReader{}, err
|
||||
}
|
||||
|
||||
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 StatsResponseReader{}, err
|
||||
}
|
||||
|
||||
return StatsResponseReader{
|
||||
return ContainerStatsResult{
|
||||
Body: resp.Body,
|
||||
OSType: resp.Header.Get("Ostype"),
|
||||
}, nil
|
||||
|
||||
Reference in New Issue
Block a user