Merge pull request #11003 from pascomnet/f_stats

stats: add a interval parameter to cli and api stats streaming
This commit is contained in:
openshift-ci[bot]
2021-08-04 09:56:57 +00:00
committed by GitHub
12 changed files with 100 additions and 29 deletions

View File

@ -5,6 +5,7 @@ import (
"os" "os"
tm "github.com/buger/goterm" tm "github.com/buger/goterm"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report" "github.com/containers/common/pkg/report"
"github.com/containers/podman/v3/cmd/podman/common" "github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/registry"
@ -16,7 +17,6 @@ import (
"github.com/containers/podman/v3/utils" "github.com/containers/podman/v3/utils"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -55,6 +55,7 @@ type statsOptionsCLI struct {
Latest bool Latest bool
NoReset bool NoReset bool
NoStream bool NoStream bool
Interval int
} }
var ( var (
@ -72,6 +73,9 @@ func statFlags(cmd *cobra.Command) {
flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen between intervals") flags.BoolVar(&statsOptions.NoReset, "no-reset", false, "Disable resetting the screen between intervals")
flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false") flags.BoolVar(&statsOptions.NoStream, "no-stream", false, "Disable streaming stats and only pull the first result, default setting is false")
intervalFlagName := "interval"
flags.IntVarP(&statsOptions.Interval, intervalFlagName, "i", 5, "Time in seconds between stats reports")
_ = cmd.RegisterFlagCompletionFunc(intervalFlagName, completion.AutocompleteNone)
} }
func init() { func init() {
@ -124,6 +128,7 @@ func stats(cmd *cobra.Command, args []string) error {
opts := entities.ContainerStatsOptions{ opts := entities.ContainerStatsOptions{
Latest: statsOptions.Latest, Latest: statsOptions.Latest,
Stream: !statsOptions.NoStream, Stream: !statsOptions.NoStream,
Interval: statsOptions.Interval,
} }
statsChan, err := registry.ContainerEngine().ContainerStats(registry.Context(), args, opts) statsChan, err := registry.ContainerEngine().ContainerStats(registry.Context(), args, opts)
if err != nil { if err != nil {
@ -134,7 +139,7 @@ func stats(cmd *cobra.Command, args []string) error {
return report.Error return report.Error
} }
if err := outputStats(report.Stats); err != nil { if err := outputStats(report.Stats); err != nil {
logrus.Error(err) return err
} }
} }
return nil return nil

View File

@ -37,6 +37,10 @@ Do not clear the terminal/screen in between reporting intervals
Disable streaming stats and only pull the first result, default setting is false Disable streaming stats and only pull the first result, default setting is false
#### **--interval**=*seconds*, **-i**=*seconds*
Time in seconds between stats reports, defaults to 5 seconds.
#### **--format**=*template* #### **--format**=*template*
Pretty-print container statistics to JSON or using a Go template Pretty-print container statistics to JSON or using a Go template

View File

@ -3,7 +3,6 @@ package libpod
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"time"
"github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod"
"github.com/containers/podman/v3/pkg/api/handlers/utils" "github.com/containers/podman/v3/pkg/api/handlers/utils"
@ -16,8 +15,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const DefaultStatsPeriod = 5 * time.Second
func StatsContainer(w http.ResponseWriter, r *http.Request) { func StatsContainer(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value("runtime").(*libpod.Runtime) runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder) decoder := r.Context().Value("decoder").(*schema.Decoder)
@ -35,8 +32,10 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) {
query := struct { query := struct {
Containers []string `schema:"containers"` Containers []string `schema:"containers"`
Stream bool `schema:"stream"` Stream bool `schema:"stream"`
Interval int `schema:"interval"`
}{ }{
Stream: true, Stream: true,
Interval: 5,
} }
if err := decoder.Decode(&query, r.URL.Query()); err != nil { if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
@ -49,6 +48,7 @@ func StatsContainer(w http.ResponseWriter, r *http.Request) {
statsOptions := entities.ContainerStatsOptions{ statsOptions := entities.ContainerStatsOptions{
Stream: query.Stream, Stream: query.Stream,
Interval: query.Interval,
} }
// Stats will stop if the connection is closed. // Stats will stop if the connection is closed.

View File

@ -1108,6 +1108,11 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
// type: boolean // type: boolean
// default: true // default: true
// description: Stream the output // description: Stream the output
// - in: query
// name: interval
// type: integer
// default: 5
// description: Time in seconds between stats reports
// produces: // produces:
// - application/json // - application/json
// responses: // responses:

View File

@ -223,6 +223,9 @@ func Stats(ctx context.Context, containers []string, options *StatsOptions) (cha
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !response.IsSuccess() {
return nil, response.Process(nil)
}
statsChan := make(chan entities.ContainerStatsReport) statsChan := make(chan entities.ContainerStatsReport)

View File

@ -167,6 +167,7 @@ type StartOptions struct {
// StatsOptions are optional options for getting stats on containers // StatsOptions are optional options for getting stats on containers
type StatsOptions struct { type StatsOptions struct {
Stream *bool Stream *bool
Interval *int
} }
//go:generate go run ../generator/generator.go TopOptions //go:generate go run ../generator/generator.go TopOptions

View File

@ -35,3 +35,19 @@ func (o *StatsOptions) GetStream() bool {
} }
return *o.Stream return *o.Stream
} }
// WithInterval
func (o *StatsOptions) WithInterval(value int) *StatsOptions {
v := &value
o.Interval = v
return o
}
// GetInterval
func (o *StatsOptions) GetInterval() int {
var interval int
if o.Interval == nil {
return interval
}
return *o.Interval
}

View File

@ -440,6 +440,8 @@ type ContainerStatsOptions struct {
Latest bool Latest bool
// Stream stats. // Stream stats.
Stream bool Stream bool
// Interval in seconds
Interval int
} }
// ContainerStatsReport is used for streaming container stats. // ContainerStatsReport is used for streaming container stats.

View File

@ -1283,6 +1283,9 @@ func (ic *ContainerEngine) Shutdown(_ context.Context) {
} }
func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []string, options entities.ContainerStatsOptions) (statsChan chan entities.ContainerStatsReport, err error) { func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []string, options entities.ContainerStatsOptions) (statsChan chan entities.ContainerStatsReport, err error) {
if options.Interval < 1 {
return nil, errors.New("Invalid interval, must be a positive number greater zero")
}
statsChan = make(chan entities.ContainerStatsReport, 1) statsChan = make(chan entities.ContainerStatsReport, 1)
containerFunc := ic.Libpod.GetRunningContainers containerFunc := ic.Libpod.GetRunningContainers
@ -1363,7 +1366,7 @@ func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []stri
return return
} }
time.Sleep(time.Second) time.Sleep(time.Second * time.Duration(options.Interval))
goto stream goto stream
}() }()

View File

@ -873,7 +873,7 @@ func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []stri
if options.Latest { if options.Latest {
return nil, errors.New("latest is not supported for the remote client") return nil, errors.New("latest is not supported for the remote client")
} }
return containers.Stats(ic.ClientCtx, namesOrIds, new(containers.StatsOptions).WithStream(options.Stream)) return containers.Stats(ic.ClientCtx, namesOrIds, new(containers.StatsOptions).WithStream(options.Stream).WithInterval(options.Interval))
} }
// ShouldRestart reports back whether the container will restart // ShouldRestart reports back whether the container will restart

View File

@ -37,19 +37,19 @@ var _ = Describe("Podman pod stats", func() {
processTestResult(f) processTestResult(f)
}) })
It("podman stats should run with no pods", func() { It("podman pod stats should run with no pods", func() {
session := podmanTest.Podman([]string{"pod", "stats", "--no-stream"}) session := podmanTest.Podman([]string{"pod", "stats", "--no-stream"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
}) })
It("podman stats with a bogus pod", func() { It("podman pod stats with a bogus pod", func() {
session := podmanTest.Podman([]string{"pod", "stats", "foobar"}) session := podmanTest.Podman([]string{"pod", "stats", "foobar"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(125)) Expect(session).Should(Exit(125))
}) })
It("podman stats on a specific running pod", func() { It("podman pod stats on a specific running pod", func() {
_, ec, podid := podmanTest.CreatePod(nil) _, ec, podid := podmanTest.CreatePod(nil)
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -66,7 +66,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).Should(Exit(0)) Expect(stats).Should(Exit(0))
}) })
It("podman stats on a specific running pod with shortID", func() { It("podman pod stats on a specific running pod with shortID", func() {
_, ec, podid := podmanTest.CreatePod(nil) _, ec, podid := podmanTest.CreatePod(nil)
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -83,7 +83,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).Should(Exit(0)) Expect(stats).Should(Exit(0))
}) })
It("podman stats on a specific running pod with name", func() { It("podman pod stats on a specific running pod with name", func() {
_, ec, podid := podmanTest.CreatePod(map[string][]string{"--name": {"test"}}) _, ec, podid := podmanTest.CreatePod(map[string][]string{"--name": {"test"}})
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -100,7 +100,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).Should(Exit(0)) Expect(stats).Should(Exit(0))
}) })
It("podman stats on running pods", func() { It("podman pod stats on running pods", func() {
_, ec, podid := podmanTest.CreatePod(nil) _, ec, podid := podmanTest.CreatePod(nil)
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -117,7 +117,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).Should(Exit(0)) Expect(stats).Should(Exit(0))
}) })
It("podman stats on all pods", func() { It("podman pod stats on all pods", func() {
_, ec, podid := podmanTest.CreatePod(nil) _, ec, podid := podmanTest.CreatePod(nil)
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -134,7 +134,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).Should(Exit(0)) Expect(stats).Should(Exit(0))
}) })
It("podman stats with json output", func() { It("podman pod stats with json output", func() {
_, ec, podid := podmanTest.CreatePod(nil) _, ec, podid := podmanTest.CreatePod(nil)
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -151,7 +151,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).Should(Exit(0)) Expect(stats).Should(Exit(0))
Expect(stats.IsJSONOutputValid()).To(BeTrue()) Expect(stats.IsJSONOutputValid()).To(BeTrue())
}) })
It("podman stats with GO template", func() { It("podman pod stats with GO template", func() {
_, ec, podid := podmanTest.CreatePod(nil) _, ec, podid := podmanTest.CreatePod(nil)
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -163,7 +163,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).To(Exit(0)) Expect(stats).To(Exit(0))
}) })
It("podman stats with invalid GO template", func() { It("podman pod stats with invalid GO template", func() {
_, ec, podid := podmanTest.CreatePod(nil) _, ec, podid := podmanTest.CreatePod(nil)
Expect(ec).To(Equal(0)) Expect(ec).To(Equal(0))
@ -175,7 +175,7 @@ var _ = Describe("Podman pod stats", func() {
Expect(stats).To(ExitWithError()) Expect(stats).To(ExitWithError())
}) })
It("podman stats on net=host post", func() { It("podman pod stats on net=host post", func() {
SkipIfRootless("--net=host not supported for rootless pods at present") SkipIfRootless("--net=host not supported for rootless pods at present")
podName := "testPod" podName := "testPod"
podCreate := podmanTest.Podman([]string{"pod", "create", "--net=host", "--name", podName}) podCreate := podmanTest.Podman([]string{"pod", "create", "--net=host", "--name", podName})

View File

@ -1,5 +1,3 @@
// +build
package integration package integration
import ( import (
@ -84,15 +82,49 @@ var _ = Describe("Podman stats", func() {
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
}) })
It("podman stats only output CPU data", func() { It("podman stats with GO template", func() {
session := podmanTest.RunTopContainer("") session := podmanTest.RunTopContainer("")
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
session = podmanTest.Podman([]string{"stats", "--all", "--no-stream", "--format", "\"{{.ID}} {{.UpTime}} {{.AVGCPU}}\""}) stats := podmanTest.Podman([]string{"stats", "-a", "--no-reset", "--no-stream", "--format", "table {{.ID}} {{.AVGCPU}} {{.MemUsage}} {{.CPU}} {{.NetIO}} {{.BlockIO}} {{.PIDS}}"})
stats.WaitWithDefaultTimeout()
Expect(stats).To(Exit(0))
})
It("podman stats with invalid GO template", func() {
session := podmanTest.RunTopContainer("")
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session.LineInOutputContains("UpTime")).To(BeTrue())
Expect(session.LineInOutputContains("AVGCPU")).To(BeTrue())
Expect(session).Should(Exit(0)) Expect(session).Should(Exit(0))
stats := podmanTest.Podman([]string{"stats", "-a", "--no-reset", "--no-stream", "--format", "\"table {{.ID}} {{.NoSuchField}} \""})
stats.WaitWithDefaultTimeout()
Expect(stats).To(ExitWithError())
})
It("podman stats with negative interval", func() {
session := podmanTest.RunTopContainer("")
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
stats := podmanTest.Podman([]string{"stats", "-a", "--no-reset", "--no-stream", "--interval=-1"})
stats.WaitWithDefaultTimeout()
Expect(stats).To(ExitWithError())
})
It("podman stats with zero interval", func() {
session := podmanTest.RunTopContainer("")
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
stats := podmanTest.Podman([]string{"stats", "-a", "--no-reset", "--no-stream", "--interval=0"})
stats.WaitWithDefaultTimeout()
Expect(stats).To(ExitWithError())
})
It("podman stats with interval", func() {
session := podmanTest.RunTopContainer("")
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
stats := podmanTest.Podman([]string{"stats", "-a", "--no-reset", "--no-stream", "--interval=5"})
stats.WaitWithDefaultTimeout()
Expect(stats).Should(Exit(0))
}) })
It("podman stats with json output", func() { It("podman stats with json output", func() {