mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Implement container stats collection in daemon
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
parent
5f5e02d22c
commit
65f58e2a74
8 changed files with 155 additions and 0 deletions
|
@ -411,6 +411,19 @@ func getContainersJSON(eng *engine.Engine, version version.Version, w http.Respo
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getContainersStats(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||||
|
if err := parseForm(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if vars == nil {
|
||||||
|
return fmt.Errorf("Missing parameter")
|
||||||
|
}
|
||||||
|
name := vars["name"]
|
||||||
|
job := eng.Job("container_stats", name)
|
||||||
|
streamJSON(job, w, true)
|
||||||
|
return job.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func getContainersLogs(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
func getContainersLogs(eng *engine.Engine, version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||||
if err := parseForm(r); err != nil {
|
if err := parseForm(r); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1323,6 +1336,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st
|
||||||
"/containers/{name:.*}/json": getContainersByName,
|
"/containers/{name:.*}/json": getContainersByName,
|
||||||
"/containers/{name:.*}/top": getContainersTop,
|
"/containers/{name:.*}/top": getContainersTop,
|
||||||
"/containers/{name:.*}/logs": getContainersLogs,
|
"/containers/{name:.*}/logs": getContainersLogs,
|
||||||
|
"/containers/{name:.*}/stats": getContainersStats,
|
||||||
"/containers/{name:.*}/attach/ws": wsContainersAttach,
|
"/containers/{name:.*}/attach/ws": wsContainersAttach,
|
||||||
"/exec/{id:.*}/json": getExecByID,
|
"/exec/{id:.*}/json": getExecByID,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1414,3 +1414,10 @@ func (container *Container) getNetworkedContainer() (*Container, error) {
|
||||||
return nil, fmt.Errorf("network mode not set to container")
|
return nil, fmt.Errorf("network mode not set to container")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (container *Container) Stats() (*execdriver.ResourceStats, error) {
|
||||||
|
if !container.IsRunning() {
|
||||||
|
return nil, fmt.Errorf("cannot collect stats on a non running container")
|
||||||
|
}
|
||||||
|
return container.daemon.Stats(container)
|
||||||
|
}
|
||||||
|
|
|
@ -104,6 +104,7 @@ type Daemon struct {
|
||||||
driver graphdriver.Driver
|
driver graphdriver.Driver
|
||||||
execDriver execdriver.Driver
|
execDriver execdriver.Driver
|
||||||
trustStore *trust.TrustStore
|
trustStore *trust.TrustStore
|
||||||
|
statsCollector *statsCollector
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install installs daemon capabilities to eng.
|
// Install installs daemon capabilities to eng.
|
||||||
|
@ -116,6 +117,7 @@ func (daemon *Daemon) Install(eng *engine.Engine) error {
|
||||||
"container_copy": daemon.ContainerCopy,
|
"container_copy": daemon.ContainerCopy,
|
||||||
"container_rename": daemon.ContainerRename,
|
"container_rename": daemon.ContainerRename,
|
||||||
"container_inspect": daemon.ContainerInspect,
|
"container_inspect": daemon.ContainerInspect,
|
||||||
|
"container_stats": daemon.ContainerStats,
|
||||||
"containers": daemon.Containers,
|
"containers": daemon.Containers,
|
||||||
"create": daemon.ContainerCreate,
|
"create": daemon.ContainerCreate,
|
||||||
"rm": daemon.ContainerRm,
|
"rm": daemon.ContainerRm,
|
||||||
|
@ -982,6 +984,7 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
|
||||||
execDriver: ed,
|
execDriver: ed,
|
||||||
eng: eng,
|
eng: eng,
|
||||||
trustStore: t,
|
trustStore: t,
|
||||||
|
statsCollector: newStatsCollector(1 * time.Second),
|
||||||
}
|
}
|
||||||
if err := daemon.restore(); err != nil {
|
if err := daemon.restore(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -1092,6 +1095,19 @@ func (daemon *Daemon) Kill(c *Container, sig int) error {
|
||||||
return daemon.execDriver.Kill(c.command, sig)
|
return daemon.execDriver.Kill(c.command, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (daemon *Daemon) Stats(c *Container) (*execdriver.ResourceStats, error) {
|
||||||
|
return daemon.execDriver.Stats(c.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (daemon *Daemon) SubscribeToContainerStats(name string) (<-chan *execdriver.ResourceStats, error) {
|
||||||
|
c := daemon.Get(name)
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("no such container")
|
||||||
|
}
|
||||||
|
ch := daemon.statsCollector.collect(c)
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Nuke kills all containers then removes all content
|
// Nuke kills all containers then removes all content
|
||||||
// from the content root, including images, volumes and
|
// from the content root, including images, volumes and
|
||||||
// container filesystems.
|
// container filesystems.
|
||||||
|
|
|
@ -5,7 +5,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/libcontainer"
|
||||||
"github.com/docker/libcontainer/devices"
|
"github.com/docker/libcontainer/devices"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,6 +63,7 @@ type Driver interface {
|
||||||
GetPidsForContainer(id string) ([]int, error) // Returns a list of pids for the given container.
|
GetPidsForContainer(id string) ([]int, error) // Returns a list of pids for the given container.
|
||||||
Terminate(c *Command) error // kill it with fire
|
Terminate(c *Command) error // kill it with fire
|
||||||
Clean(id string) error // clean all traces of container exec
|
Clean(id string) error // clean all traces of container exec
|
||||||
|
Stats(id string) (*ResourceStats, error) // Get resource stats for a running container
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network settings of the container
|
// Network settings of the container
|
||||||
|
@ -101,6 +104,12 @@ type Resources struct {
|
||||||
Cpuset string `json:"cpuset"`
|
Cpuset string `json:"cpuset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResourceStats struct {
|
||||||
|
*libcontainer.ContainerStats
|
||||||
|
Read time.Time `json:"read"`
|
||||||
|
ClockTicks int `json:"clock_ticks"`
|
||||||
|
}
|
||||||
|
|
||||||
type Mount struct {
|
type Mount struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Destination string `json:"destination"`
|
Destination string `json:"destination"`
|
||||||
|
|
|
@ -524,3 +524,8 @@ func (t *TtyConsole) Close() error {
|
||||||
func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) {
|
func (d *driver) Exec(c *execdriver.Command, processConfig *execdriver.ProcessConfig, pipes *execdriver.Pipes, startCallback execdriver.StartCallback) (int, error) {
|
||||||
return -1, ErrExec
|
return -1, ErrExec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) {
|
||||||
|
return nil, fmt.Errorf("container stats are not support with LXC")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/docker/daemon/execdriver"
|
"github.com/docker/docker/daemon/execdriver"
|
||||||
|
@ -279,6 +280,23 @@ func (d *driver) Clean(id string) error {
|
||||||
return os.RemoveAll(filepath.Join(d.root, id))
|
return os.RemoveAll(filepath.Join(d.root, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *driver) Stats(id string) (*execdriver.ResourceStats, error) {
|
||||||
|
state, err := libcontainer.GetState(filepath.Join(d.root, id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
stats, err := libcontainer.GetStats(nil, state)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &execdriver.ResourceStats{
|
||||||
|
ContainerStats: stats,
|
||||||
|
ClockTicks: system.GetClockTicks(),
|
||||||
|
Read: now,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getEnv(key string, env []string) string {
|
func getEnv(key string, env []string) string {
|
||||||
for _, pair := range env {
|
for _, pair := range env {
|
||||||
parts := strings.Split(pair, "=")
|
parts := strings.Split(pair, "=")
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -77,3 +78,17 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig.
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (daemon *Daemon) ContainerStats(job *engine.Job) engine.Status {
|
||||||
|
stats, err := daemon.SubscribeToContainerStats(job.Args[0])
|
||||||
|
if err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(job.Stdout)
|
||||||
|
for update := range stats {
|
||||||
|
if err := enc.Encode(update); err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return engine.StatusOK
|
||||||
|
}
|
||||||
|
|
71
daemon/stats_collector.go
Normal file
71
daemon/stats_collector.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/daemon/execdriver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStatsCollector(interval time.Duration) *statsCollector {
|
||||||
|
s := &statsCollector{
|
||||||
|
interval: interval,
|
||||||
|
containers: make(map[string]*statsCollectorData),
|
||||||
|
}
|
||||||
|
s.start()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type statsCollectorData struct {
|
||||||
|
c *Container
|
||||||
|
lastStats *execdriver.ResourceStats
|
||||||
|
subs []chan *execdriver.ResourceStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// statsCollector manages and provides container resource stats
|
||||||
|
type statsCollector struct {
|
||||||
|
m sync.Mutex
|
||||||
|
interval time.Duration
|
||||||
|
containers map[string]*statsCollectorData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statsCollector) collect(c *Container) <-chan *execdriver.ResourceStats {
|
||||||
|
s.m.Lock()
|
||||||
|
ch := make(chan *execdriver.ResourceStats, 1024)
|
||||||
|
s.containers[c.ID] = &statsCollectorData{
|
||||||
|
c: c,
|
||||||
|
subs: []chan *execdriver.ResourceStats{
|
||||||
|
ch,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s.m.Unlock()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statsCollector) stopCollection(c *Container) {
|
||||||
|
s.m.Lock()
|
||||||
|
delete(s.containers, c.ID)
|
||||||
|
s.m.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *statsCollector) start() {
|
||||||
|
go func() {
|
||||||
|
for _ = range time.Tick(s.interval) {
|
||||||
|
log.Debugf("starting collection of container stats")
|
||||||
|
s.m.Lock()
|
||||||
|
for id, d := range s.containers {
|
||||||
|
stats, err := d.c.Stats()
|
||||||
|
if err != nil {
|
||||||
|
// TODO: @crosbymichael evict container depending on error
|
||||||
|
log.Errorf("collecting stats for %s: %v", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, sub := range s.containers[id].subs {
|
||||||
|
sub <- stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.m.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
Loading…
Reference in a new issue