diff --git a/pkg/api/types.go b/pkg/api/types.go index 2a5c550f937..bdaec3756ae 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -147,20 +147,6 @@ type Container struct { LivenessProbe *LivenessProbe `yaml:"livenessProbe,omitempty" json:"livenessProbe,omitempty"` } -// Percentile represents a pair which contains a percentage from 0 to 100 and -// its corresponding value. -type Percentile struct { - Percentage int `json:"percentage,omitempty"` - Value uint64 `json:"value,omitempty"` -} - -// ContainerStats represents statistical information of a container -type ContainerStats struct { - CpuUsagePercentiles []Percentile `json:"cpu_usage_percentiles,omitempty"` - MemoryUsagePercentiles []Percentile `json:"memory_usage_percentiles,omitempty"` - MaxMemoryUsage uint64 `json:"max_memory_usage,omitempty"` -} - // Event is the representation of an event logged to etcd backends type Event struct { Event string `json:"event,omitempty"` diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index a80e8bcc794..24c2c62cf20 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -54,7 +54,7 @@ const milliCpuToCpu = 1000 // CadvisorInterface is an abstract interface for testability. It abstracts the interface of "github.com/google/cadvisor/client".Client. type CadvisorInterface interface { - ContainerInfo(name string) (*info.ContainerInfo, error) + ContainerInfo(name string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) MachineInfo() (*info.MachineInfo, error) } @@ -844,44 +844,29 @@ func (kl *Kubelet) getDockerIDFromPodIDAndContainerName(podID, containerName str return "", errors.New("couldn't find container") } +func getCadvisorContainerInfoRequest(req *info.ContainerInfoRequest) *info.ContainerInfoRequest { + ret := &info.ContainerInfoRequest{ + NumStats: req.NumStats, + CpuUsagePercentiles: req.CpuUsagePercentiles, + MemoryUsagePercentages: req.MemoryUsagePercentages, + } + return ret +} + // This method takes a container's absolute path and returns the stats for the // container. The container's absolute path refers to its hierarchy in the // cgroup file system. e.g. The root container, which represents the whole // machine, has path "/"; all docker containers have path "/docker/" -func (kl *Kubelet) statsFromContainerPath(containerPath string) (*api.ContainerStats, error) { - info, err := kl.CadvisorClient.ContainerInfo(containerPath) - +func (kl *Kubelet) statsFromContainerPath(containerPath string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { + cinfo, err := kl.CadvisorClient.ContainerInfo(containerPath, getCadvisorContainerInfoRequest(req)) if err != nil { return nil, err } - // When the stats data for the container is not available yet. - if info.StatsPercentiles == nil { - return nil, nil - } - - ret := new(api.ContainerStats) - ret.MaxMemoryUsage = info.StatsPercentiles.MaxMemoryUsage - if len(info.StatsPercentiles.CpuUsagePercentiles) > 0 { - percentiles := make([]api.Percentile, len(info.StatsPercentiles.CpuUsagePercentiles)) - for i, p := range info.StatsPercentiles.CpuUsagePercentiles { - percentiles[i].Percentage = p.Percentage - percentiles[i].Value = p.Value - } - ret.CpuUsagePercentiles = percentiles - } - if len(info.StatsPercentiles.MemoryUsagePercentiles) > 0 { - percentiles := make([]api.Percentile, len(info.StatsPercentiles.MemoryUsagePercentiles)) - for i, p := range info.StatsPercentiles.MemoryUsagePercentiles { - percentiles[i].Percentage = p.Percentage - percentiles[i].Value = p.Value - } - ret.MemoryUsagePercentiles = percentiles - } - return ret, nil + return cinfo, nil } // GetContainerStats returns stats (from Cadvisor) for a container. -func (kl *Kubelet) GetContainerStats(podID, containerName string) (*api.ContainerStats, error) { +func (kl *Kubelet) GetContainerInfo(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { if kl.CadvisorClient == nil { return nil, nil } @@ -889,12 +874,12 @@ func (kl *Kubelet) GetContainerStats(podID, containerName string) (*api.Containe if err != nil || len(dockerID) == 0 { return nil, err } - return kl.statsFromContainerPath(fmt.Sprintf("/docker/%s", string(dockerID))) + return kl.statsFromContainerPath(fmt.Sprintf("/docker/%s", string(dockerID)), req) } // GetMachineStats returns stats (from Cadvisor) of current machine. -func (kl *Kubelet) GetMachineStats() (*api.ContainerStats, error) { - return kl.statsFromContainerPath("/") +func (kl *Kubelet) GetMachineStats(req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { + return kl.statsFromContainerPath("/", req) } func (kl *Kubelet) healthy(container api.Container, dockerContainer *docker.APIContainers) (health.Status, error) { diff --git a/pkg/kubelet/kubelet_server.go b/pkg/kubelet/kubelet_server.go index adfc13e283e..2bd33c5046c 100644 --- a/pkg/kubelet/kubelet_server.go +++ b/pkg/kubelet/kubelet_server.go @@ -20,6 +20,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net/http" "net/url" @@ -28,6 +29,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/google/cadvisor/info" "gopkg.in/v1/yaml" ) @@ -41,8 +43,8 @@ type KubeletServer struct { // kubeletInterface contains all the kubelet methods required by the server. // For testablitiy. type kubeletInterface interface { - GetContainerStats(podID, containerName string) (*api.ContainerStats, error) - GetMachineStats() (*api.ContainerStats, error) + GetContainerInfo(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) + GetMachineStats(req *info.ContainerInfoRequest) (*info.ContainerInfo, error) GetPodInfo(name string) (api.PodInfo, error) } @@ -113,18 +115,25 @@ func (s *KubeletServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (s *KubeletServer) serveStats(w http.ResponseWriter, req *http.Request) { // /stats// components := strings.Split(strings.TrimPrefix(path.Clean(req.URL.Path), "/"), "/") - var stats *api.ContainerStats + var stats *info.ContainerInfo var err error + var query info.ContainerInfoRequest + decoder := json.NewDecoder(req.Body) + err = decoder.Decode(&query) + if err != nil && err != io.EOF { + s.error(w, err) + return + } switch len(components) { case 1: // Machine stats - stats, err = s.Kubelet.GetMachineStats() + stats, err = s.Kubelet.GetMachineStats(&query) case 2: // pod stats // TODO(monnand) Implement this errors.New("pod level status currently unimplemented") case 3: - stats, err = s.Kubelet.GetContainerStats(components[1], components[2]) + stats, err = s.Kubelet.GetContainerInfo(components[1], components[2], &query) default: http.Error(w, "unknown resource.", http.StatusNotFound) return diff --git a/pkg/kubelet/kubelet_server_test.go b/pkg/kubelet/kubelet_server_test.go index bb00612ba95..fce6913be8e 100644 --- a/pkg/kubelet/kubelet_server_test.go +++ b/pkg/kubelet/kubelet_server_test.go @@ -29,24 +29,25 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/fsouza/go-dockerclient" + "github.com/google/cadvisor/info" ) type fakeKubelet struct { infoFunc func(name string) (api.PodInfo, error) - containerStatsFunc func(podID, containerName string) (*api.ContainerStats, error) - machineStatsFunc func() (*api.ContainerStats, error) + containerStatsFunc func(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) + machineStatsFunc func(query *info.ContainerInfoRequest) (*info.ContainerInfo, error) } func (fk *fakeKubelet) GetPodInfo(name string) (api.PodInfo, error) { return fk.infoFunc(name) } -func (fk *fakeKubelet) GetContainerStats(podID, containerName string) (*api.ContainerStats, error) { - return fk.containerStatsFunc(podID, containerName) +func (fk *fakeKubelet) GetContainerInfo(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { + return fk.containerStatsFunc(podID, containerName, req) } -func (fk *fakeKubelet) GetMachineStats() (*api.ContainerStats, error) { - return fk.machineStatsFunc() +func (fk *fakeKubelet) GetMachineStats(req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { + return fk.machineStatsFunc(req) } type serverTestFramework struct { @@ -148,22 +149,24 @@ func TestPodInfo(t *testing.T) { func TestContainerStats(t *testing.T) { fw := makeServerTest() - expectedStats := &api.ContainerStats{ - MaxMemoryUsage: 1024001, - CpuUsagePercentiles: []api.Percentile{ - {50, 150}, - {80, 180}, - {90, 190}, - }, - MemoryUsagePercentiles: []api.Percentile{ - {50, 150}, - {80, 180}, - {90, 190}, + expectedStats := &info.ContainerInfo{ + StatsPercentiles: &info.ContainerStatsPercentiles{ + MaxMemoryUsage: 1024001, + CpuUsagePercentiles: []info.Percentile{ + {50, 150}, + {80, 180}, + {90, 190}, + }, + MemoryUsagePercentiles: []info.Percentile{ + {50, 150}, + {80, 180}, + {90, 190}, + }, }, } expectedPodID := "somepod" expectedContainerName := "goodcontainer" - fw.fakeKubelet.containerStatsFunc = func(podID, containerName string) (*api.ContainerStats, error) { + fw.fakeKubelet.containerStatsFunc = func(podID, containerName string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { if podID != expectedPodID || containerName != expectedContainerName { return nil, fmt.Errorf("bad podID or containerName: podID=%v; containerName=%v", podID, containerName) } @@ -175,7 +178,7 @@ func TestContainerStats(t *testing.T) { t.Fatalf("Got error GETing: %v", err) } defer resp.Body.Close() - var receivedStats api.ContainerStats + var receivedStats info.ContainerInfo decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&receivedStats) if err != nil { @@ -188,20 +191,22 @@ func TestContainerStats(t *testing.T) { func TestMachineStats(t *testing.T) { fw := makeServerTest() - expectedStats := &api.ContainerStats{ - MaxMemoryUsage: 1024001, - CpuUsagePercentiles: []api.Percentile{ - {50, 150}, - {80, 180}, - {90, 190}, - }, - MemoryUsagePercentiles: []api.Percentile{ - {50, 150}, - {80, 180}, - {90, 190}, + expectedStats := &info.ContainerInfo{ + StatsPercentiles: &info.ContainerStatsPercentiles{ + MaxMemoryUsage: 1024001, + CpuUsagePercentiles: []info.Percentile{ + {50, 150}, + {80, 180}, + {90, 190}, + }, + MemoryUsagePercentiles: []info.Percentile{ + {50, 150}, + {80, 180}, + {90, 190}, + }, }, } - fw.fakeKubelet.machineStatsFunc = func() (*api.ContainerStats, error) { + fw.fakeKubelet.machineStatsFunc = func(req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { return expectedStats, nil } @@ -210,7 +215,7 @@ func TestMachineStats(t *testing.T) { t.Fatalf("Got error GETing: %v", err) } defer resp.Body.Close() - var receivedStats api.ContainerStats + var receivedStats info.ContainerInfo decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&receivedStats) if err != nil { diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 143c861b72e..0d32062f734 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -912,8 +912,8 @@ type mockCadvisorClient struct { } // ContainerInfo is a mock implementation of CadvisorInterface.ContainerInfo. -func (c *mockCadvisorClient) ContainerInfo(name string) (*info.ContainerInfo, error) { - args := c.Called(name) +func (c *mockCadvisorClient) ContainerInfo(name string, req *info.ContainerInfoRequest) (*info.ContainerInfo, error) { + args := c.Called(name, req) return args.Get(0).(*info.ContainerInfo), args.Error(1) } @@ -925,7 +925,7 @@ func (c *mockCadvisorClient) MachineInfo() (*info.MachineInfo, error) { func areSamePercentiles( cadvisorPercentiles []info.Percentile, - kubePercentiles []api.Percentile, + kubePercentiles []info.Percentile, t *testing.T, ) { if len(cadvisorPercentiles) != len(kubePercentiles) { @@ -974,7 +974,9 @@ func TestGetContainerStats(t *testing.T) { } mockCadvisor := &mockCadvisorClient{} - mockCadvisor.On("ContainerInfo", containerPath).Return(containerInfo, nil) + req := &info.ContainerInfoRequest{} + cadvisorReq := getCadvisorContainerInfoRequest(req) + mockCadvisor.On("ContainerInfo", containerPath, cadvisorReq).Return(containerInfo, nil) kubelet, _, fakeDocker := makeTestKubelet(t) kubelet.CadvisorClient = mockCadvisor @@ -987,15 +989,15 @@ func TestGetContainerStats(t *testing.T) { }, } - stats, err := kubelet.GetContainerStats("qux", "foo") + stats, err := kubelet.GetContainerInfo("qux", "foo", req) if err != nil { t.Errorf("unexpected error: %v", err) } - if stats.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage { + if stats.StatsPercentiles.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage { t.Errorf("wrong max memory usage") } - areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.CpuUsagePercentiles, t) - areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.MemoryUsagePercentiles, t) + areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.StatsPercentiles.CpuUsagePercentiles, t) + areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.StatsPercentiles.MemoryUsagePercentiles, t) mockCadvisor.AssertExpectations(t) } @@ -1019,7 +1021,9 @@ func TestGetMachineStats(t *testing.T) { } mockCadvisor := &mockCadvisorClient{} - mockCadvisor.On("ContainerInfo", containerPath).Return(containerInfo, nil) + req := &info.ContainerInfoRequest{} + cadvisorReq := getCadvisorContainerInfoRequest(req) + mockCadvisor.On("ContainerInfo", containerPath, cadvisorReq).Return(containerInfo, nil) kubelet := Kubelet{ DockerClient: &fakeDocker, @@ -1028,15 +1032,15 @@ func TestGetMachineStats(t *testing.T) { } // If the container name is an empty string, then it means the root container. - stats, err := kubelet.GetMachineStats() + stats, err := kubelet.GetMachineStats(req) if err != nil { t.Errorf("unexpected error: %v", err) } - if stats.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage { + if stats.StatsPercentiles.MaxMemoryUsage != containerInfo.StatsPercentiles.MaxMemoryUsage { t.Errorf("wrong max memory usage") } - areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.CpuUsagePercentiles, t) - areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.MemoryUsagePercentiles, t) + areSamePercentiles(containerInfo.StatsPercentiles.CpuUsagePercentiles, stats.StatsPercentiles.CpuUsagePercentiles, t) + areSamePercentiles(containerInfo.StatsPercentiles.MemoryUsagePercentiles, stats.StatsPercentiles.MemoryUsagePercentiles, t) mockCadvisor.AssertExpectations(t) } @@ -1051,19 +1055,19 @@ func TestGetContainerStatsWithoutCadvisor(t *testing.T) { }, } - stats, _ := kubelet.GetContainerStats("qux", "foo") + stats, _ := kubelet.GetContainerInfo("qux", "foo", nil) // When there's no cAdvisor, the stats should be either nil or empty if stats == nil { return } - if stats.MaxMemoryUsage != 0 { - t.Errorf("MaxMemoryUsage is %v even if there's no cadvisor", stats.MaxMemoryUsage) + if stats.StatsPercentiles.MaxMemoryUsage != 0 { + t.Errorf("MaxMemoryUsage is %v even if there's no cadvisor", stats.StatsPercentiles.MaxMemoryUsage) } - if len(stats.CpuUsagePercentiles) > 0 { - t.Errorf("Cpu usage percentiles is not empty (%+v) even if there's no cadvisor", stats.CpuUsagePercentiles) + if len(stats.StatsPercentiles.CpuUsagePercentiles) > 0 { + t.Errorf("Cpu usage percentiles is not empty (%+v) even if there's no cadvisor", stats.StatsPercentiles.CpuUsagePercentiles) } - if len(stats.MemoryUsagePercentiles) > 0 { - t.Errorf("Memory usage percentiles is not empty (%+v) even if there's no cadvisor", stats.MemoryUsagePercentiles) + if len(stats.StatsPercentiles.MemoryUsagePercentiles) > 0 { + t.Errorf("Memory usage percentiles is not empty (%+v) even if there's no cadvisor", stats.StatsPercentiles.MemoryUsagePercentiles) } } @@ -1073,8 +1077,10 @@ func TestGetContainerStatsWhenCadvisorFailed(t *testing.T) { containerInfo := &info.ContainerInfo{} mockCadvisor := &mockCadvisorClient{} + req := &info.ContainerInfoRequest{} + cadvisorReq := getCadvisorContainerInfoRequest(req) expectedErr := fmt.Errorf("some error") - mockCadvisor.On("ContainerInfo", containerPath).Return(containerInfo, expectedErr) + mockCadvisor.On("ContainerInfo", containerPath, cadvisorReq).Return(containerInfo, expectedErr) kubelet, _, fakeDocker := makeTestKubelet(t) kubelet.CadvisorClient = mockCadvisor @@ -1087,7 +1093,7 @@ func TestGetContainerStatsWhenCadvisorFailed(t *testing.T) { }, } - stats, err := kubelet.GetContainerStats("qux", "foo") + stats, err := kubelet.GetContainerInfo("qux", "foo", req) if stats != nil { t.Errorf("non-nil stats on error") } @@ -1108,7 +1114,7 @@ func TestGetContainerStatsOnNonExistContainer(t *testing.T) { kubelet.CadvisorClient = mockCadvisor fakeDocker.containerList = []docker.APIContainers{} - stats, _ := kubelet.GetContainerStats("qux", "foo") + stats, _ := kubelet.GetContainerInfo("qux", "foo", nil) if stats != nil { t.Errorf("non-nil stats on non exist container") } diff --git a/third_party/deps.sh b/third_party/deps.sh index 5f63f14c5d9..36c1de723ff 100755 --- a/third_party/deps.sh +++ b/third_party/deps.sh @@ -5,6 +5,8 @@ TOP_PACKAGES=" code.google.com/p/goauth2/compute/serviceaccount code.google.com/p/goauth2/oauth code.google.com/p/google-api-go-client/compute/v1 + github.com/google/cadvisor/info + github.com/google/cadvisor/client " DEP_PACKAGES=" @@ -13,7 +15,6 @@ DEP_PACKAGES=" code.google.com/p/google-api-go-client/googleapi github.com/coreos/go-log/log github.com/coreos/go-systemd/journal - github.com/google/cadvisor/info " PACKAGES="$TOP_PACKAGES $DEP_PACKAGES" diff --git a/third_party/src/github.com/google/cadvisor/.gitignore b/third_party/src/github.com/google/cadvisor/.gitignore new file mode 100644 index 00000000000..1377554ebea --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/third_party/src/github.com/google/cadvisor/.travis.yml b/third_party/src/github.com/google/cadvisor/.travis.yml index 8542bd19106..ed54fedc5d6 100644 --- a/third_party/src/github.com/google/cadvisor/.travis.yml +++ b/third_party/src/github.com/google/cadvisor/.travis.yml @@ -1,13 +1,12 @@ language: go go: - - 1.2 + - 1.3 before_script: - go get github.com/stretchr/testify/mock - go get github.com/kr/pretty + - wget http://s3.amazonaws.com/influxdb/influxdb_latest_amd64.deb + - sudo dpkg -i influxdb_latest_amd64.deb + - sudo service influxdb start script: - - go test -v -race github.com/google/cadvisor/container - - go test -v github.com/google/cadvisor/info - - go test -v github.com/google/cadvisor/client - - go test -v github.com/google/cadvisor/sampling - - go test -v github.com/google/cadvisor/storage/memory + - go test -v -race github.com/google/cadvisor/... - go build github.com/google/cadvisor diff --git a/third_party/src/github.com/google/cadvisor/CHANGELOG.md b/third_party/src/github.com/google/cadvisor/CHANGELOG.md new file mode 100644 index 00000000000..32d8b2ec2e0 --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## 0.1.2 (2014-07-10) +- Added Storage Driver concept (flag: storage_driver), default is the in-memory driver +- Implemented InfluxDB storage driver +- Support in REST API for specifying number of stats to return +- Allow running without lmctfy (flag: allow_lmctfy) +- Bugfixes + +## 0.1.0 (2014-06-14) +- Support for container aliases +- Sampling historical usage and exporting that in the REST API +- Bugfixes for UI + +## 0.0.0 (2014-06-10) +- Initial version of cAdvisor +- Web UI with auto-updating stats +- v1.0 REST API with container and machine information +- Support for Docker containers +- Support for lmctfy containers diff --git a/third_party/src/github.com/google/cadvisor/CONTRIBUTORS b/third_party/src/github.com/google/cadvisor/CONTRIBUTORS index d0c97f1ad9f..cb65c25e88d 100644 --- a/third_party/src/github.com/google/cadvisor/CONTRIBUTORS +++ b/third_party/src/github.com/google/cadvisor/CONTRIBUTORS @@ -6,6 +6,10 @@ # Please keep the list sorted by first name. +Jason Swindle +Johan Euphrosine Kamil Yurtsever Nan Deng +Rohit Jnagal Victor Marmol +Zohaib Maya diff --git a/third_party/src/github.com/google/cadvisor/advice/interference/detector.go b/third_party/src/github.com/google/cadvisor/advice/interference/detector.go new file mode 100644 index 00000000000..8c29176311c --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/advice/interference/detector.go @@ -0,0 +1,32 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package inference + +import "github.com/google/cadvisor/info" + +// InterferenceDectector detects if there's a container which +// interferences with a set of containers. The detector tracks +// a set of containers and find the victims and antagonist. +type InterferenceDetector interface { + // Tracks the behavior of the container. + AddContainer(ref info.ContainerReference) + + // Returns a list of possible interferences. The upper layer may take action + // based on the interference. + Detect() ([]*info.Interference, error) + + // The name of the detector. + Name() string +} diff --git a/third_party/src/github.com/google/cadvisor/api/handler.go b/third_party/src/github.com/google/cadvisor/api/handler.go index 409dc917791..393705bde04 100644 --- a/third_party/src/github.com/google/cadvisor/api/handler.go +++ b/third_party/src/github.com/google/cadvisor/api/handler.go @@ -19,12 +19,13 @@ package api import ( "encoding/json" "fmt" + "io" "log" "net/http" - "net/url" "strings" "time" + "github.com/google/cadvisor/info" "github.com/google/cadvisor/manager" ) @@ -34,9 +35,11 @@ const ( MachineApi = "machine" ) -func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error { +func HandleRequest(m manager.Manager, w http.ResponseWriter, r *http.Request) error { start := time.Now() + u := r.URL + // Get API request type. requestType := u.Path[len(ApiResource):] i := strings.Index(requestType, "/") @@ -46,7 +49,8 @@ func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error { requestType = requestType[:i] } - if requestType == MachineApi { + switch { + case requestType == MachineApi: log.Printf("Api - Machine") // Get the MachineInfo @@ -60,14 +64,20 @@ func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error { fmt.Fprintf(w, "Failed to marshall MachineInfo with error: %s", err) } w.Write(out) - } else if requestType == ContainersApi { + case requestType == ContainersApi: // The container name is the path after the requestType containerName := requestArgs log.Printf("Api - Container(%s)", containerName) + var query info.ContainerInfoRequest + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&query) + if err != nil && err != io.EOF { + return fmt.Errorf("unable to decode the json value: ", err) + } // Get the container. - cont, err := m.GetContainerInfo(containerName) + cont, err := m.GetContainerInfo(containerName, &query) if err != nil { fmt.Fprintf(w, "Failed to get container \"%s\" with error: %s", containerName, err) return err @@ -79,7 +89,7 @@ func HandleRequest(m manager.Manager, w http.ResponseWriter, u *url.URL) error { fmt.Fprintf(w, "Failed to marshall container %q with error: %s", containerName, err) } w.Write(out) - } else { + default: return fmt.Errorf("unknown API request type %q", requestType) } diff --git a/third_party/src/github.com/google/cadvisor/cadvisor.go b/third_party/src/github.com/google/cadvisor/cadvisor.go index b6fa9f917b5..5df6e27d24d 100644 --- a/third_party/src/github.com/google/cadvisor/cadvisor.go +++ b/third_party/src/github.com/google/cadvisor/cadvisor.go @@ -27,32 +27,46 @@ import ( "github.com/google/cadvisor/manager" "github.com/google/cadvisor/pages" "github.com/google/cadvisor/pages/static" - "github.com/google/cadvisor/storage/memory" ) var argPort = flag.Int("port", 8080, "port to listen") -var argSampleSize = flag.Int("samples", 1024, "number of samples we want to keep") -var argHistoryDuration = flag.Int("history_duration", 60, "number of seconds of container history to keep") +var argAllowLmctfy = flag.Bool("allow_lmctfy", true, "whether to allow lmctfy as a container handler") + +var argDbDriver = flag.String("storage_driver", "memory", "storage driver to use. Options are: memory (default) and influxdb") func main() { flag.Parse() - storage := memory.New(*argSampleSize, *argHistoryDuration) - // TODO(monnand): Add stats writer for manager - containerManager, err := manager.New(storage) + storageDriver, err := NewStorageDriver(*argDbDriver) + if err != nil { + log.Fatalf("Failed to connect to database: %s", err) + } + + containerManager, err := manager.New(storageDriver) if err != nil { log.Fatalf("Failed to create a Container Manager: %s", err) } - if err := lmctfy.Register("/"); err != nil { - log.Printf("lmctfy registration failed: %v.", err) - log.Print("Running in docker only mode.") - if err := docker.Register(containerManager, "/"); err != nil { - log.Printf("Docker registration failed: %v.", err) - log.Fatalf("Unable to continue without docker or lmctfy.") + // Register lmctfy for the root if allowed and available. + registeredRoot := false + if *argAllowLmctfy { + if err := lmctfy.Register("/"); err != nil { + log.Printf("lmctfy registration failed: %v.", err) + log.Print("Running in docker only mode.") + } else { + registeredRoot = true } } + // Register Docker for root if we were unable to register lmctfy. + if !registeredRoot { + if err := docker.Register(containerManager, "/"); err != nil { + log.Printf("Docker registration failed: %v.", err) + log.Fatalf("Unable to continue without root handler.") + } + } + + // Register Docker for all Docker containers. if err := docker.Register(containerManager, "/docker"); err != nil { // Ignore this error because we should work with lmctfy only log.Printf("Docker registration failed: %v.", err) @@ -69,7 +83,7 @@ func main() { // Handler for the API. http.HandleFunc(api.ApiResource, func(w http.ResponseWriter, r *http.Request) { - err := api.HandleRequest(containerManager, w, r.URL) + err := api.HandleRequest(containerManager, w, r) if err != nil { fmt.Fprintf(w, "%s", err) } diff --git a/third_party/src/github.com/google/cadvisor/client/client.go b/third_party/src/github.com/google/cadvisor/client/client.go index d521b1e3fa8..033cbc2a31e 100644 --- a/third_party/src/github.com/google/cadvisor/client/client.go +++ b/third_party/src/github.com/google/cadvisor/client/client.go @@ -15,6 +15,7 @@ package cadvisor import ( + "bytes" "encoding/json" "fmt" "io/ioutil" @@ -49,7 +50,7 @@ func (self *Client) machineInfoUrl() string { func (self *Client) MachineInfo() (minfo *info.MachineInfo, err error) { u := self.machineInfoUrl() ret := new(info.MachineInfo) - err = self.httpGetJsonData(ret, u, "machine info") + err = self.httpGetJsonData(ret, nil, u, "machine info") if err != nil { return } @@ -64,8 +65,19 @@ func (self *Client) containerInfoUrl(name string) string { return strings.Join([]string{self.baseUrl, "containers", name}, "/") } -func (self *Client) httpGetJsonData(data interface{}, url, infoName string) error { - resp, err := http.Get(url) +func (self *Client) httpGetJsonData(data, postData interface{}, url, infoName string) error { + var resp *http.Response + var err error + + if postData != nil { + data, err := json.Marshal(postData) + if err != nil { + return fmt.Errorf("unable to marshal data: %v", err) + } + resp, err = http.Post(url, "application/json", bytes.NewBuffer(data)) + } else { + resp, err = http.Get(url) + } if err != nil { err = fmt.Errorf("unable to get %v: %v", infoName, err) return err @@ -84,10 +96,12 @@ func (self *Client) httpGetJsonData(data interface{}, url, infoName string) erro return nil } -func (self *Client) ContainerInfo(name string) (cinfo *info.ContainerInfo, err error) { +func (self *Client) ContainerInfo( + name string, + query *info.ContainerInfoRequest) (cinfo *info.ContainerInfo, err error) { u := self.containerInfoUrl(name) ret := new(info.ContainerInfo) - err = self.httpGetJsonData(ret, u, fmt.Sprintf("container info for %v", name)) + err = self.httpGetJsonData(ret, query, u, fmt.Sprintf("container info for %v", name)) if err != nil { return } diff --git a/third_party/src/github.com/google/cadvisor/client/client_test.go b/third_party/src/github.com/google/cadvisor/client/client_test.go index d02607302b4..a5b7f69113a 100644 --- a/third_party/src/github.com/google/cadvisor/client/client_test.go +++ b/third_party/src/github.com/google/cadvisor/client/client_test.go @@ -21,33 +21,42 @@ import ( "net/http/httptest" "reflect" "testing" + "time" "github.com/google/cadvisor/info" + itest "github.com/google/cadvisor/info/test" + "github.com/kr/pretty" ) func testGetJsonData( - strRep string, - emptyData interface{}, + expected interface{}, f func() (interface{}, error), ) error { - err := json.Unmarshal([]byte(strRep), emptyData) - if err != nil { - return fmt.Errorf("invalid json input: %v", err) - } reply, err := f() if err != nil { return fmt.Errorf("unable to retrieve data: %v", err) } - if !reflect.DeepEqual(reply, emptyData) { - return fmt.Errorf("retrieved wrong data: %+v != %+v", reply, emptyData) + if !reflect.DeepEqual(reply, expected) { + return pretty.Errorf("retrieved wrong data: %# v != %# v", reply, expected) } return nil } -func cadvisorTestClient(path, reply string) (*Client, *httptest.Server, error) { +func cadvisorTestClient(path string, expectedPostObj, expectedPostObjEmpty, replyObj interface{}, t *testing.T) (*Client, *httptest.Server, error) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == path { - fmt.Fprint(w, reply) + if expectedPostObj != nil { + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(expectedPostObjEmpty) + if err != nil { + t.Errorf("Recieved invalid object: %v", err) + } + if !reflect.DeepEqual(expectedPostObj, expectedPostObjEmpty) { + t.Errorf("Recieved unexpected object: %+v", expectedPostObjEmpty) + } + } + encoder := json.NewEncoder(w) + encoder.Encode(replyObj) } else if r.URL.Path == "/api/v1.0/machine" { fmt.Fprint(w, `{"num_cores":8,"memory_capacity":31625871360}`) } else { @@ -64,693 +73,69 @@ func cadvisorTestClient(path, reply string) (*Client, *httptest.Server, error) { } func TestGetMachineinfo(t *testing.T) { - respStr := `{"num_cores":8,"memory_capacity":31625871360}` - client, server, err := cadvisorTestClient("/api/v1.0/machine", respStr) + minfo := &info.MachineInfo{ + NumCores: 8, + MemoryCapacity: 31625871360, + } + client, server, err := cadvisorTestClient("/api/v1.0/machine", nil, nil, minfo, t) if err != nil { t.Fatalf("unable to get a client %v", err) } defer server.Close() - err = testGetJsonData(respStr, &info.MachineInfo{}, func() (interface{}, error) { - return client.MachineInfo() - }) + returned, err := client.MachineInfo() if err != nil { t.Fatal(err) } + if !reflect.DeepEqual(returned, minfo) { + t.Fatalf("received unexpected machine info") + } } func TestGetContainerInfo(t *testing.T) { - respStr := ` -{ - "name": "%v", - "spec": { - "cpu": { - "limit": 18446744073709551000, - "max_limit": 0, - "mask": { - "data": [ - 18446744073709551000 - ] - } - }, - "memory": { - "limit": 18446744073709551000, - "swap_limit": 18446744073709551000 - } - }, - "stats": [ - { - "timestamp": "2014-06-13T01:03:26.434981825Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:27.538394608Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:28.640302072Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:29.74247308Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:30.844494537Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:31.946757066Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:33.050214062Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:34.15222186Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:35.25417391Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:36.355902169Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:37.457585928Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:38.559417379Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:39.662978029Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:40.764671232Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - }, - { - "timestamp": "2014-06-13T01:03:41.866456459Z", - "cpu": { - "usage": { - "total": 56896502, - "per_cpu": [ - 20479682, - 13579420, - 6025040, - 2255123, - 3635661, - 2489368, - 5158288, - 3273920 - ], - "user": 10000000, - "system": 30000000 - }, - "load": 0 - }, - "memory": { - "usage": 495616, - "container_data": { - "pgfault": 2279 - }, - "hierarchical_data": { - "pgfault": 2279 - } - } - } - ], - "stats_summary": { - "max_memory_usage": 495616, - "samples": [ - { - "timestamp": "2014-06-13T01:03:27.538394608Z", - "duration": 1103412783, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:28.640302072Z", - "duration": 1101907464, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:29.74247308Z", - "duration": 1102171008, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:30.844494537Z", - "duration": 1102021457, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:31.946757066Z", - "duration": 1102262529, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:33.050214062Z", - "duration": 1103456996, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:34.15222186Z", - "duration": 1102007798, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:35.25417391Z", - "duration": 1101952050, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:36.355902169Z", - "duration": 1101728259, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:37.457585928Z", - "duration": 1101683759, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:38.559417379Z", - "duration": 1101831451, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:39.662978029Z", - "duration": 1103560650, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:40.764671232Z", - "duration": 1101693203, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - }, - { - "timestamp": "2014-06-13T01:03:41.866456459Z", - "duration": 1101785227, - "cpu": { - "usage": 0 - }, - "memory": { - "usage": 495616 - } - } - ], - "memory_usage_percentiles": [ - { - "percentage": 50, - "value": 495616 - }, - { - "percentage": 80, - "value": 495616 - }, - { - "percentage": 90, - "value": 495616 - }, - { - "percentage": 95, - "value": 495616 - }, - { - "percentage": 99, - "value": 495616 - } - ], - "cpu_usage_percentiles": [ - { - "percentage": 50, - "value": 0 - }, - { - "percentage": 80, - "value": 0 - }, - { - "percentage": 90, - "value": 0 - }, - { - "percentage": 95, - "value": 0 - }, - { - "percentage": 99, - "value": 0 - } - ] - } -} -` + query := &info.ContainerInfoRequest{ + NumStats: 3, + NumSamples: 2, + CpuUsagePercentiles: []int{10, 50, 90}, + MemoryUsagePercentages: []int{10, 80, 90}, + } containerName := "/some/container" - respStr = fmt.Sprintf(respStr, containerName) - client, server, err := cadvisorTestClient(fmt.Sprintf("/api/v1.0/containers%v", containerName), respStr) + cinfo := itest.GenerateRandomContainerInfo(containerName, 4, query, 1*time.Second) + client, server, err := cadvisorTestClient(fmt.Sprintf("/api/v1.0/containers%v", containerName), query, &info.ContainerInfoRequest{}, cinfo, t) if err != nil { t.Fatalf("unable to get a client %v", err) } defer server.Close() - err = testGetJsonData(respStr, &info.ContainerInfo{}, func() (interface{}, error) { - return client.ContainerInfo(containerName) - }) + returned, err := client.ContainerInfo(containerName, query) if err != nil { t.Fatal(err) } + + // We cannot use DeepEqual() to compare them directly, + // because json en/decoded time may have precision issues. + if !reflect.DeepEqual(returned.ContainerReference, cinfo.ContainerReference) { + t.Errorf("received unexpected container ref") + } + if !reflect.DeepEqual(returned.Subcontainers, cinfo.Subcontainers) { + t.Errorf("received unexpected subcontainers") + } + if !reflect.DeepEqual(returned.Spec, cinfo.Spec) { + t.Errorf("received unexpected spec") + } + if !reflect.DeepEqual(returned.StatsPercentiles, cinfo.StatsPercentiles) { + t.Errorf("received unexpected spec") + } + + for i, expectedStats := range cinfo.Stats { + returnedStats := returned.Stats[i] + if !expectedStats.Eq(returnedStats) { + t.Errorf("received unexpected stats") + } + } + + for i, expectedSample := range cinfo.Samples { + returnedSample := returned.Samples[i] + if !expectedSample.Eq(returnedSample) { + t.Errorf("received unexpected sample") + } + } } diff --git a/third_party/src/github.com/google/cadvisor/container/docker/handler.go b/third_party/src/github.com/google/cadvisor/container/docker/handler.go index 5c8fc10bcc0..f6456a4cf2e 100644 --- a/third_party/src/github.com/google/cadvisor/container/docker/handler.go +++ b/third_party/src/github.com/google/cadvisor/container/docker/handler.go @@ -118,7 +118,7 @@ func (self *dockerContainerHandler) isDockerContainer() bool { } // TODO(vmarmol): Switch to getting this from libcontainer once we have a solid API. -func readLibcontainerSpec(id string) (spec *libcontainer.Container, err error) { +func readLibcontainerSpec(id string) (spec *libcontainer.Config, err error) { dir := "/var/lib/docker/execdriver/native" configPath := path.Join(dir, id, "container.json") f, err := os.Open(configPath) @@ -127,7 +127,7 @@ func readLibcontainerSpec(id string) (spec *libcontainer.Container, err error) { } defer f.Close() d := json.NewDecoder(f) - ret := new(libcontainer.Container) + ret := new(libcontainer.Config) err = d.Decode(ret) if err != nil { return @@ -136,7 +136,7 @@ func readLibcontainerSpec(id string) (spec *libcontainer.Container, err error) { return } -func libcontainerConfigToContainerSpec(config *libcontainer.Container, mi *info.MachineInfo) *info.ContainerSpec { +func libcontainerConfigToContainerSpec(config *libcontainer.Config, mi *info.MachineInfo) *info.ContainerSpec { spec := new(info.ContainerSpec) spec.Memory = new(info.MemorySpec) spec.Memory.Limit = math.MaxUint64 @@ -209,6 +209,12 @@ func libcontainerToContainerStats(s *cgroups.Stats, mi *info.MachineInfo) *info. ret.Memory.ContainerData.Pgmajfault = v ret.Memory.HierarchicalData.Pgmajfault = v } + if v, ok := s.MemoryStats.Stats["total_inactive_anon"]; ok { + ret.Memory.WorkingSet = ret.Memory.Usage - v + if v, ok := s.MemoryStats.Stats["total_active_file"]; ok { + ret.Memory.WorkingSet -= v + } + } return ret } diff --git a/third_party/src/github.com/google/cadvisor/container/test/mock.go b/third_party/src/github.com/google/cadvisor/container/test/mock.go new file mode 100644 index 00000000000..fbbe8fe0ac0 --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/container/test/mock.go @@ -0,0 +1,88 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "github.com/google/cadvisor/container" + "github.com/google/cadvisor/info" + "github.com/stretchr/testify/mock" +) + +// This struct mocks a container handler. +type MockContainerHandler struct { + mock.Mock + Name string + Aliases []string +} + +// If self.Name is not empty, then ContainerReference() will return self.Name and self.Aliases. +// Otherwise, it will use the value provided by .On().Return(). +func (self *MockContainerHandler) ContainerReference() (info.ContainerReference, error) { + if len(self.Name) > 0 { + var aliases []string + if len(self.Aliases) > 0 { + aliases = make([]string, len(self.Aliases)) + copy(aliases, self.Aliases) + } + return info.ContainerReference{ + Name: self.Name, + Aliases: aliases, + }, nil + } + args := self.Called() + return args.Get(0).(info.ContainerReference), args.Error(1) +} + +func (self *MockContainerHandler) GetSpec() (*info.ContainerSpec, error) { + args := self.Called() + return args.Get(0).(*info.ContainerSpec), args.Error(1) +} + +func (self *MockContainerHandler) GetStats() (*info.ContainerStats, error) { + args := self.Called() + return args.Get(0).(*info.ContainerStats), args.Error(1) +} + +func (self *MockContainerHandler) ListContainers(listType container.ListType) ([]info.ContainerReference, error) { + args := self.Called(listType) + return args.Get(0).([]info.ContainerReference), args.Error(1) +} + +func (self *MockContainerHandler) ListThreads(listType container.ListType) ([]int, error) { + args := self.Called(listType) + return args.Get(0).([]int), args.Error(1) +} + +func (self *MockContainerHandler) ListProcesses(listType container.ListType) ([]int, error) { + args := self.Called(listType) + return args.Get(0).([]int), args.Error(1) +} + +type FactoryForMockContainerHandler struct { + Name string + PrepareContainerHandlerFunc func(name string, handler *MockContainerHandler) +} + +func (self *FactoryForMockContainerHandler) String() string { + return self.Name +} + +func (self *FactoryForMockContainerHandler) NewContainerHandler(name string) (container.ContainerHandler, error) { + handler := &MockContainerHandler{} + if self.PrepareContainerHandlerFunc != nil { + self.PrepareContainerHandlerFunc(name, handler) + } + return handler, nil +} diff --git a/third_party/src/github.com/google/cadvisor/deploy/lmctfy-docker/Dockerfile b/third_party/src/github.com/google/cadvisor/deploy/lmctfy-docker/Dockerfile index ce9f76de324..38f03fcc63e 100644 --- a/third_party/src/github.com/google/cadvisor/deploy/lmctfy-docker/Dockerfile +++ b/third_party/src/github.com/google/cadvisor/deploy/lmctfy-docker/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y -q --no-install-recommends pkg-config l # Get the lcmtfy and cAdvisor binaries. ADD http://storage.googleapis.com/cadvisor-bin/lmctfy/lmctfy /usr/bin/lmctfy ADD http://storage.googleapis.com/cadvisor-bin/lmctfy/libre2.so.0.0.0 /usr/lib/libre2.so.0 -ADD http://storage.googleapis.com/cadvisor-bin/cadvisor /usr/bin/cadvisor +ADD http://storage.googleapis.com/cadvisor-bin/cadvisor-0.1.2 /usr/bin/cadvisor RUN chmod +x /usr/bin/lmctfy && chmod +x /usr/bin/cadvisor EXPOSE 8080 diff --git a/third_party/src/github.com/google/cadvisor/info/advice.go b/third_party/src/github.com/google/cadvisor/info/advice.go new file mode 100644 index 00000000000..8084cf4741f --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/info/advice.go @@ -0,0 +1,34 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package info + +// This struct describes one type of relationship between containers: One +// container, antagonist, interferes the performance of other +// containers, victims. +type Interference struct { + // Absolute name of the antagonist container name. This field + // should not be empty. + Antagonist string `json:"antagonist"` + + // The absolute path of the victims. This field should not be empty. + Victims []string `json:"victims"` + + // The name of the detector used to detect this antagonism. This field + // should not be empty + Detector string `json:"detector"` + + // Human readable description of this interference + Description string `json:"description,omitempty"` +} diff --git a/third_party/src/github.com/google/cadvisor/info/container.go b/third_party/src/github.com/google/cadvisor/info/container.go index 94c7c6574c5..cd6a717502e 100644 --- a/third_party/src/github.com/google/cadvisor/info/container.go +++ b/third_party/src/github.com/google/cadvisor/info/container.go @@ -16,6 +16,7 @@ package info import ( "fmt" + "reflect" "sort" "time" ) @@ -57,6 +58,40 @@ type ContainerReference struct { Aliases []string `json:"aliases,omitempty"` } +// ContainerInfoQuery is used when users check a container info from the REST api. +// It specifies how much data users want to get about a container +type ContainerInfoRequest struct { + // Max number of stats to return. + NumStats int `json:"num_stats,omitempty"` + // Max number of samples to return. + NumSamples int `json:"num_samples,omitempty"` + + // Different percentiles of CPU usage within a period. The values must be within [0, 100] + CpuUsagePercentiles []int `json:"cpu_usage_percentiles,omitempty"` + // Different percentiles of memory usage within a period. The values must be within [0, 100] + MemoryUsagePercentages []int `json:"memory_usage_percentiles,omitempty"` +} + +func (self *ContainerInfoRequest) FillDefaults() *ContainerInfoRequest { + ret := self + if ret == nil { + ret = new(ContainerInfoRequest) + } + if ret.NumStats <= 0 { + ret.NumStats = 1024 + } + if ret.NumSamples <= 0 { + ret.NumSamples = 1024 + } + if len(ret.CpuUsagePercentiles) == 0 { + ret.CpuUsagePercentiles = []int{50, 80, 90, 99} + } + if len(ret.MemoryUsagePercentages) == 0 { + ret.MemoryUsagePercentages = []int{50, 80, 90, 99} + } + return ret +} + type ContainerInfo struct { ContainerReference @@ -119,7 +154,7 @@ type CpuStats struct { // Per CPU/core usage of the container. // Unit: nanoseconds. - PerCpu []uint64 `json:"per_cpu,omitempty"` + PerCpu []uint64 `json:"per_cpu_usage,omitempty"` // Time spent in user space. // Unit: nanoseconds @@ -165,6 +200,42 @@ type ContainerStats struct { Memory *MemoryStats `json:"memory,omitempty"` } +// Makes a deep copy of the ContainerStats and returns a pointer to the new +// copy. Copy() will allocate a new ContainerStats object if dst is nil. +func (self *ContainerStats) Copy(dst *ContainerStats) *ContainerStats { + if dst == nil { + dst = new(ContainerStats) + } + dst.Timestamp = self.Timestamp + if self.Cpu != nil { + if dst.Cpu == nil { + dst.Cpu = new(CpuStats) + } + // To make a deep copy of a slice, we need to copy every value + // in the slice. To make less memory allocation, we would like + // to reuse the slice in dst if possible. + percpu := dst.Cpu.Usage.PerCpu + if len(percpu) != len(self.Cpu.Usage.PerCpu) { + percpu = make([]uint64, len(self.Cpu.Usage.PerCpu)) + } + dst.Cpu.Usage = self.Cpu.Usage + dst.Cpu.Load = self.Cpu.Load + copy(percpu, self.Cpu.Usage.PerCpu) + dst.Cpu.Usage.PerCpu = percpu + } else { + dst.Cpu = nil + } + if self.Memory != nil { + if dst.Memory == nil { + dst.Memory = new(MemoryStats) + } + *dst.Memory = *self.Memory + } else { + dst.Memory = nil + } + return dst +} + type ContainerStatsSample struct { // Timetamp of the end of the sample period Timestamp time.Time `json:"timestamp"` @@ -173,6 +244,9 @@ type ContainerStatsSample struct { Cpu struct { // number of nanoseconds of CPU time used by the container Usage uint64 `json:"usage"` + + // Per-core usage of the container. (unit: nanoseconds) + PerCpuUsage []uint64 `json:"per_cpu_usage,omitempty"` } `json:"cpu"` Memory struct { // Units: Bytes. @@ -180,6 +254,67 @@ type ContainerStatsSample struct { } `json:"memory"` } +func timeEq(t1, t2 time.Time, tolerance time.Duration) bool { + // t1 should not be later than t2 + if t1.After(t2) { + t1, t2 = t2, t1 + } + diff := t2.Sub(t1) + if diff <= tolerance { + return true + } + return false +} + +func durationEq(a, b time.Duration, tolerance time.Duration) bool { + if a > b { + a, b = b, a + } + diff := a - b + if diff <= tolerance { + return true + } + return false +} + +const ( + // 10ms, i.e. 0.01s + timePrecision time.Duration = 10 * time.Millisecond +) + +// This function is useful because we do not require precise time +// representation. +func (a *ContainerStats) Eq(b *ContainerStats) bool { + if !timeEq(a.Timestamp, b.Timestamp, timePrecision) { + return false + } + if !reflect.DeepEqual(a.Cpu, b.Cpu) { + return false + } + if !reflect.DeepEqual(a.Memory, b.Memory) { + return false + } + return true +} + +// This function is useful because we do not require precise time +// representation. +func (a *ContainerStatsSample) Eq(b *ContainerStatsSample) bool { + if !timeEq(a.Timestamp, b.Timestamp, timePrecision) { + return false + } + if !durationEq(a.Duration, b.Duration, timePrecision) { + return false + } + if !reflect.DeepEqual(a.Cpu, b.Cpu) { + return false + } + if !reflect.DeepEqual(a.Memory, b.Memory) { + return false + } + return true +} + type Percentile struct { Percentage int `json:"percentage"` Value uint64 `json:"value"` @@ -211,9 +346,28 @@ func NewSample(prev, current *ContainerStats) (*ContainerStatsSample, error) { if current.Cpu.Usage.Total < prev.Cpu.Usage.Total { return nil, fmt.Errorf("current CPU usage is less than prev CPU usage (cumulative).") } + + var percpu []uint64 + + if len(current.Cpu.Usage.PerCpu) > 0 { + curNumCpus := len(current.Cpu.Usage.PerCpu) + percpu = make([]uint64, curNumCpus) + + for i, currUsage := range current.Cpu.Usage.PerCpu { + var prevUsage uint64 = 0 + if i < len(prev.Cpu.Usage.PerCpu) { + prevUsage = prev.Cpu.Usage.PerCpu[i] + } + if currUsage < prevUsage { + return nil, fmt.Errorf("current per-core CPU usage is less than prev per-core CPU usage (cumulative).") + } + percpu[i] = currUsage - prevUsage + } + } sample := new(ContainerStatsSample) // Calculate the diff to get the CPU usage within the time interval. sample.Cpu.Usage = current.Cpu.Usage.Total - prev.Cpu.Usage.Total + sample.Cpu.PerCpuUsage = percpu // Memory usage is current memory usage sample.Memory.Usage = current.Memory.Usage sample.Timestamp = current.Timestamp diff --git a/third_party/src/github.com/google/cadvisor/info/container_test.go b/third_party/src/github.com/google/cadvisor/info/container_test.go index 4b275becc62..4b3c84a2423 100644 --- a/third_party/src/github.com/google/cadvisor/info/container_test.go +++ b/third_party/src/github.com/google/cadvisor/info/container_test.go @@ -15,6 +15,7 @@ package info import ( + "reflect" "testing" "time" ) @@ -230,3 +231,68 @@ func TestAddSampleWrongCpuUsage(t *testing.T) { t.Errorf("generated an unexpected sample: %+v", sample) } } + +func TestAddSampleHotPluggingCpu(t *testing.T) { + cpuPrevUsage := uint64(10) + cpuCurrentUsage := uint64(15) + memCurrentUsage := uint64(200) + prevTime := time.Now() + + prev := createStats(cpuPrevUsage, memCurrentUsage, prevTime) + current := createStats(cpuCurrentUsage, memCurrentUsage, prevTime.Add(1*time.Second)) + current.Cpu.Usage.PerCpu = append(current.Cpu.Usage.PerCpu, 10) + + sample, err := NewSample(prev, current) + if err != nil { + t.Errorf("should be able to generate a sample. but received error: %v", err) + } + if len(sample.Cpu.PerCpuUsage) != 2 { + t.Fatalf("Should have 2 cores.") + } + if sample.Cpu.PerCpuUsage[0] != cpuCurrentUsage-cpuPrevUsage { + t.Errorf("First cpu usage is %v. should be %v", sample.Cpu.PerCpuUsage[0], cpuCurrentUsage-cpuPrevUsage) + } + if sample.Cpu.PerCpuUsage[1] != 10 { + t.Errorf("Second cpu usage is %v. should be 10", sample.Cpu.PerCpuUsage[1]) + } +} + +func TestAddSampleHotUnpluggingCpu(t *testing.T) { + cpuPrevUsage := uint64(10) + cpuCurrentUsage := uint64(15) + memCurrentUsage := uint64(200) + prevTime := time.Now() + + prev := createStats(cpuPrevUsage, memCurrentUsage, prevTime) + current := createStats(cpuCurrentUsage, memCurrentUsage, prevTime.Add(1*time.Second)) + prev.Cpu.Usage.PerCpu = append(prev.Cpu.Usage.PerCpu, 10) + + sample, err := NewSample(prev, current) + if err != nil { + t.Errorf("should be able to generate a sample. but received error: %v", err) + } + if len(sample.Cpu.PerCpuUsage) != 1 { + t.Fatalf("Should have 1 cores.") + } + if sample.Cpu.PerCpuUsage[0] != cpuCurrentUsage-cpuPrevUsage { + t.Errorf("First cpu usage is %v. should be %v", sample.Cpu.PerCpuUsage[0], cpuCurrentUsage-cpuPrevUsage) + } +} + +func TestContainerStatsCopy(t *testing.T) { + stats := createStats(100, 101, time.Now()) + shadowStats := stats.Copy(nil) + if !reflect.DeepEqual(stats, shadowStats) { + t.Errorf("Copy() returned different object") + } + stats.Cpu.Usage.PerCpu[0] = shadowStats.Cpu.Usage.PerCpu[0] + 1 + stats.Cpu.Load = shadowStats.Cpu.Load + 1 + stats.Memory.Usage = shadowStats.Memory.Usage + 1 + if reflect.DeepEqual(stats, shadowStats) { + t.Errorf("Copy() did not deeply copy the object") + } + stats = shadowStats.Copy(stats) + if !reflect.DeepEqual(stats, shadowStats) { + t.Errorf("Copy() returned different object") + } +} diff --git a/third_party/src/github.com/google/cadvisor/info/test/datagen.go b/third_party/src/github.com/google/cadvisor/info/test/datagen.go new file mode 100644 index 00000000000..a1c0a3565ca --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/info/test/datagen.go @@ -0,0 +1,127 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "fmt" + "math" + "math/rand" + "time" + + "github.com/google/cadvisor/info" +) + +func GenerateRandomStats(numStats, numCores int, duration time.Duration) []*info.ContainerStats { + ret := make([]*info.ContainerStats, numStats) + perCoreUsages := make([]uint64, numCores) + currentTime := time.Now() + for i := range perCoreUsages { + perCoreUsages[i] = uint64(rand.Int63n(1000)) + } + for i := 0; i < numStats; i++ { + stats := new(info.ContainerStats) + stats.Cpu = new(info.CpuStats) + stats.Memory = new(info.MemoryStats) + stats.Timestamp = currentTime + currentTime = currentTime.Add(duration) + + percore := make([]uint64, numCores) + for i := range perCoreUsages { + perCoreUsages[i] += uint64(rand.Int63n(1000)) + percore[i] = perCoreUsages[i] + stats.Cpu.Usage.Total += percore[i] + } + stats.Cpu.Usage.PerCpu = percore + stats.Cpu.Usage.User = stats.Cpu.Usage.Total + stats.Cpu.Usage.System = 0 + stats.Memory.Usage = uint64(rand.Int63n(4096)) + ret[i] = stats + } + return ret +} + +func GenerateRandomContainerSpec(numCores int) *info.ContainerSpec { + ret := &info.ContainerSpec{ + Cpu: &info.CpuSpec{}, + Memory: &info.MemorySpec{}, + } + ret.Cpu.Limit = uint64(1000 + rand.Int63n(2000)) + ret.Cpu.MaxLimit = uint64(1000 + rand.Int63n(2000)) + n := (numCores + 63) / 64 + ret.Cpu.Mask.Data = make([]uint64, n) + for i := 0; i < n; i++ { + ret.Cpu.Mask.Data[i] = math.MaxUint64 + } + + ret.Memory.Limit = uint64(4096 + rand.Int63n(4096)) + return ret +} + +func GenerateRandomContainerInfo(containerName string, numCores int, query *info.ContainerInfoRequest, duration time.Duration) *info.ContainerInfo { + stats := GenerateRandomStats(query.NumStats, numCores, duration) + samples, _ := NewSamplesFromStats(stats...) + if len(samples) > query.NumSamples { + samples = samples[:query.NumSamples] + } + cpuPercentiles := make([]info.Percentile, 0, len(query.CpuUsagePercentiles)) + + // TODO(monnand): This will generate percentiles where 50%tile data may + // be larger than 90%tile data. + for _, p := range query.CpuUsagePercentiles { + percentile := info.Percentile{p, uint64(rand.Int63n(1000))} + cpuPercentiles = append(cpuPercentiles, percentile) + } + memPercentiles := make([]info.Percentile, 0, len(query.MemoryUsagePercentages)) + for _, p := range query.MemoryUsagePercentages { + percentile := info.Percentile{p, uint64(rand.Int63n(1000))} + memPercentiles = append(memPercentiles, percentile) + } + + percentiles := &info.ContainerStatsPercentiles{ + MaxMemoryUsage: uint64(rand.Int63n(4096)), + MemoryUsagePercentiles: memPercentiles, + CpuUsagePercentiles: cpuPercentiles, + } + + spec := GenerateRandomContainerSpec(numCores) + + ret := &info.ContainerInfo{ + ContainerReference: info.ContainerReference{ + Name: containerName, + }, + Spec: spec, + StatsPercentiles: percentiles, + Samples: samples, + Stats: stats, + } + return ret +} + +func NewSamplesFromStats(stats ...*info.ContainerStats) ([]*info.ContainerStatsSample, error) { + if len(stats) < 2 { + return nil, nil + } + samples := make([]*info.ContainerStatsSample, 0, len(stats)-1) + for i, s := range stats[1:] { + prev := stats[i] + sample, err := info.NewSample(prev, s) + if err != nil { + return nil, fmt.Errorf("Unable to generate sample from %+v and %+v: %v", + prev, s, err) + } + samples = append(samples, sample) + } + return samples, nil +} diff --git a/third_party/src/github.com/google/cadvisor/info/version.go b/third_party/src/github.com/google/cadvisor/info/version.go index a00a0222fe6..96d233c9d24 100644 --- a/third_party/src/github.com/google/cadvisor/info/version.go +++ b/third_party/src/github.com/google/cadvisor/info/version.go @@ -15,4 +15,4 @@ package info // Version of cAdvisor. -const VERSION = "0.1.0" +const VERSION = "0.1.2" diff --git a/third_party/src/github.com/google/cadvisor/manager/container_test.go b/third_party/src/github.com/google/cadvisor/manager/container_test.go new file mode 100644 index 00000000000..c942eb81cab --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/manager/container_test.go @@ -0,0 +1,237 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Per-container manager. + +package manager + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/google/cadvisor/container" + ctest "github.com/google/cadvisor/container/test" + "github.com/google/cadvisor/info" + itest "github.com/google/cadvisor/info/test" + "github.com/google/cadvisor/storage" + stest "github.com/google/cadvisor/storage/test" +) + +func createContainerDataAndSetHandler( + driver storage.StorageDriver, + f func(*ctest.MockContainerHandler), + t *testing.T, +) *containerData { + factory := &ctest.FactoryForMockContainerHandler{ + Name: "factoryForMockContainer", + PrepareContainerHandlerFunc: func(name string, handler *ctest.MockContainerHandler) { + handler.Name = name + f(handler) + }, + } + container.RegisterContainerHandlerFactory("/", factory) + + if driver == nil { + driver = &stest.MockStorageDriver{} + } + + ret, err := NewContainerData("/container", driver) + if err != nil { + t.Fatal(err) + } + return ret +} + +func TestContainerUpdateSubcontainers(t *testing.T) { + var handler *ctest.MockContainerHandler + subcontainers := []info.ContainerReference{ + {Name: "/container/ee0103"}, + {Name: "/container/abcd"}, + {Name: "/container/something"}, + } + cd := createContainerDataAndSetHandler( + nil, + func(h *ctest.MockContainerHandler) { + h.On("ListContainers", container.LIST_SELF).Return( + subcontainers, + nil, + ) + handler = h + }, + t, + ) + + err := cd.updateSubcontainers() + if err != nil { + t.Fatal(err) + } + + if len(cd.info.Subcontainers) != len(subcontainers) { + t.Errorf("Received %v subcontainers, should be %v", len(cd.info.Subcontainers), len(subcontainers)) + } + + for _, sub := range cd.info.Subcontainers { + found := false + for _, sub2 := range subcontainers { + if sub.Name == sub2.Name { + found = true + } + } + if !found { + t.Errorf("Received unknown sub container %v", sub) + } + } + + handler.AssertExpectations(t) +} + +func TestContainerUpdateSubcontainersWithError(t *testing.T) { + var handler *ctest.MockContainerHandler + cd := createContainerDataAndSetHandler( + nil, + func(h *ctest.MockContainerHandler) { + h.On("ListContainers", container.LIST_SELF).Return( + []info.ContainerReference{}, + fmt.Errorf("some error"), + ) + handler = h + }, + t, + ) + + err := cd.updateSubcontainers() + if err == nil { + t.Fatal("updateSubcontainers should return error") + } + if len(cd.info.Subcontainers) != 0 { + t.Errorf("Received %v subcontainers, should be 0", len(cd.info.Subcontainers)) + } + + handler.AssertExpectations(t) +} + +func TestContainerUpdateStats(t *testing.T) { + var handler *ctest.MockContainerHandler + var ref info.ContainerReference + + driver := &stest.MockStorageDriver{} + + statsList := itest.GenerateRandomStats(1, 4, 1*time.Second) + stats := statsList[0] + + cd := createContainerDataAndSetHandler( + driver, + func(h *ctest.MockContainerHandler) { + h.On("GetStats").Return( + stats, + nil, + ) + handler = h + ref.Name = h.Name + }, + t, + ) + + driver.On("AddStats", ref, stats).Return(nil) + + err := cd.updateStats() + if err != nil { + t.Fatal(err) + } + + handler.AssertExpectations(t) +} + +func TestContainerUpdateSpec(t *testing.T) { + var handler *ctest.MockContainerHandler + spec := itest.GenerateRandomContainerSpec(4) + cd := createContainerDataAndSetHandler( + nil, + func(h *ctest.MockContainerHandler) { + h.On("GetSpec").Return( + spec, + nil, + ) + handler = h + }, + t, + ) + + err := cd.updateSpec() + if err != nil { + t.Fatal(err) + } + + handler.AssertExpectations(t) +} + +func TestContainerGetInfo(t *testing.T) { + var handler *ctest.MockContainerHandler + spec := itest.GenerateRandomContainerSpec(4) + subcontainers := []info.ContainerReference{ + {Name: "/container/ee0103"}, + {Name: "/container/abcd"}, + {Name: "/container/something"}, + } + aliases := []string{"a1", "a2"} + cd := createContainerDataAndSetHandler( + nil, + func(h *ctest.MockContainerHandler) { + h.On("GetSpec").Return( + spec, + nil, + ) + h.On("ListContainers", container.LIST_SELF).Return( + subcontainers, + nil, + ) + h.Aliases = aliases + handler = h + }, + t, + ) + + info, err := cd.GetInfo() + if err != nil { + t.Fatal(err) + } + + handler.AssertExpectations(t) + + if len(info.Subcontainers) != len(subcontainers) { + t.Errorf("Received %v subcontainers, should be %v", len(info.Subcontainers), len(subcontainers)) + } + + for _, sub := range info.Subcontainers { + found := false + for _, sub2 := range subcontainers { + if sub.Name == sub2.Name { + found = true + } + } + if !found { + t.Errorf("Received unknown sub container %v", sub) + } + } + + if !reflect.DeepEqual(spec, info.Spec) { + t.Errorf("received wrong container spec") + } + + if info.Name != handler.Name { + t.Errorf("received wrong container name: received %v; should be %v", info.Name, handler.Name) + } +} diff --git a/third_party/src/github.com/google/cadvisor/manager/manager.go b/third_party/src/github.com/google/cadvisor/manager/manager.go index 1b0503551c4..696e0badbdf 100644 --- a/third_party/src/github.com/google/cadvisor/manager/manager.go +++ b/third_party/src/github.com/google/cadvisor/manager/manager.go @@ -30,7 +30,7 @@ type Manager interface { Start() error // Get information about a container. - GetContainerInfo(containerName string) (*info.ContainerInfo, error) + GetContainerInfo(containerName string, query *info.ContainerInfoRequest) (*info.ContainerInfo, error) // Get information about the machine. GetMachineInfo() (*info.MachineInfo, error) @@ -106,8 +106,8 @@ func (m *manager) Start() error { } // Get a container by name. -func (m *manager) GetContainerInfo(containerName string) (*info.ContainerInfo, error) { - log.Printf("Get(%s)", containerName) +func (m *manager) GetContainerInfo(containerName string, query *info.ContainerInfoRequest) (*info.ContainerInfo, error) { + log.Printf("Get(%s); %+v", containerName, query) var cont *containerData var ok bool func() { @@ -130,21 +130,21 @@ func (m *manager) GetContainerInfo(containerName string) (*info.ContainerInfo, e var percentiles *info.ContainerStatsPercentiles var samples []*info.ContainerStatsSample var stats []*info.ContainerStats - // TODO(monnand): These numbers should not be hard coded + query = query.FillDefaults() percentiles, err = m.storageDriver.Percentiles( cinfo.Name, - []int{50, 80, 90, 99}, - []int{50, 80, 90, 99}, + query.CpuUsagePercentiles, + query.MemoryUsagePercentages, ) if err != nil { return nil, err } - samples, err = m.storageDriver.Samples(cinfo.Name, 1024) + samples, err = m.storageDriver.Samples(cinfo.Name, query.NumSamples) if err != nil { return nil, err } - stats, err = m.storageDriver.RecentStats(cinfo.Name, 1024) + stats, err = m.storageDriver.RecentStats(cinfo.Name, query.NumStats) if err != nil { return nil, err } diff --git a/third_party/src/github.com/google/cadvisor/manager/manager_test.go b/third_party/src/github.com/google/cadvisor/manager/manager_test.go new file mode 100644 index 00000000000..99862c5db07 --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/manager/manager_test.go @@ -0,0 +1,253 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Per-container manager. + +package manager + +import ( + "reflect" + "testing" + "time" + + "github.com/google/cadvisor/container" + ctest "github.com/google/cadvisor/container/test" + "github.com/google/cadvisor/info" + itest "github.com/google/cadvisor/info/test" + stest "github.com/google/cadvisor/storage/test" +) + +func createManagerAndAddContainers( + driver *stest.MockStorageDriver, + containers []string, + f func(*ctest.MockContainerHandler), + t *testing.T, +) *manager { + if driver == nil { + driver = &stest.MockStorageDriver{} + } + factory := &ctest.FactoryForMockContainerHandler{ + Name: "factoryForManager", + PrepareContainerHandlerFunc: func(name string, handler *ctest.MockContainerHandler) { + handler.Name = name + found := false + for _, c := range containers { + if c == name { + found = true + } + } + if !found { + t.Errorf("Asked to create a container with name %v, which is unknown.", name) + } + f(handler) + }, + } + container.RegisterContainerHandlerFactory("/", factory) + mif, err := New(driver) + if err != nil { + t.Fatal(err) + } + if ret, ok := mif.(*manager); ok { + for _, container := range containers { + ret.containers[container], err = NewContainerData(container, driver) + if err != nil { + t.Fatal(err) + } + } + return ret + } + t.Fatal("Wrong type") + return nil +} + +func TestGetContainerInfo(t *testing.T) { + containers := []string{ + "/c1", + "/c2", + } + + query := &info.ContainerInfoRequest{ + NumStats: 256, + NumSamples: 128, + CpuUsagePercentiles: []int{10, 50, 90}, + MemoryUsagePercentages: []int{10, 80, 90}, + } + + infosMap := make(map[string]*info.ContainerInfo, len(containers)) + handlerMap := make(map[string]*ctest.MockContainerHandler, len(containers)) + + for _, container := range containers { + infosMap[container] = itest.GenerateRandomContainerInfo(container, 4, query, 1*time.Second) + } + + driver := &stest.MockStorageDriver{} + m := createManagerAndAddContainers( + driver, + containers, + func(h *ctest.MockContainerHandler) { + cinfo := infosMap[h.Name] + stats := cinfo.Stats + samples := cinfo.Samples + percentiles := cinfo.StatsPercentiles + spec := cinfo.Spec + driver.On( + "Percentiles", + h.Name, + query.CpuUsagePercentiles, + query.MemoryUsagePercentages, + ).Return( + percentiles, + nil, + ) + + driver.On( + "Samples", + h.Name, + query.NumSamples, + ).Return( + samples, + nil, + ) + + driver.On( + "RecentStats", + h.Name, + query.NumStats, + ).Return( + stats, + nil, + ) + + h.On("ListContainers", container.LIST_SELF).Return( + []info.ContainerReference(nil), + nil, + ) + h.On("GetSpec").Return( + spec, + nil, + ) + handlerMap[h.Name] = h + }, + t, + ) + + returnedInfos := make(map[string]*info.ContainerInfo, len(containers)) + + for _, container := range containers { + cinfo, err := m.GetContainerInfo(container, query) + if err != nil { + t.Fatalf("Unable to get info for container %v: %v", container, err) + } + returnedInfos[container] = cinfo + } + + for container, handler := range handlerMap { + handler.AssertExpectations(t) + returned := returnedInfos[container] + expected := infosMap[container] + if !reflect.DeepEqual(returned, expected) { + t.Errorf("returned unexpected info for container %v; returned %+v; expected %+v", container, returned, expected) + } + } + +} + +func TestGetContainerInfoWithDefaultValue(t *testing.T) { + containers := []string{ + "/c1", + "/c2", + } + + var query *info.ContainerInfoRequest + query = query.FillDefaults() + + infosMap := make(map[string]*info.ContainerInfo, len(containers)) + handlerMap := make(map[string]*ctest.MockContainerHandler, len(containers)) + + for _, container := range containers { + infosMap[container] = itest.GenerateRandomContainerInfo(container, 4, query, 1*time.Second) + } + + driver := &stest.MockStorageDriver{} + m := createManagerAndAddContainers( + driver, + containers, + func(h *ctest.MockContainerHandler) { + cinfo := infosMap[h.Name] + stats := cinfo.Stats + samples := cinfo.Samples + percentiles := cinfo.StatsPercentiles + spec := cinfo.Spec + driver.On( + "Percentiles", + h.Name, + query.CpuUsagePercentiles, + query.MemoryUsagePercentages, + ).Return( + percentiles, + nil, + ) + + driver.On( + "Samples", + h.Name, + query.NumSamples, + ).Return( + samples, + nil, + ) + + driver.On( + "RecentStats", + h.Name, + query.NumStats, + ).Return( + stats, + nil, + ) + + h.On("ListContainers", container.LIST_SELF).Return( + []info.ContainerReference(nil), + nil, + ) + h.On("GetSpec").Return( + spec, + nil, + ) + handlerMap[h.Name] = h + }, + t, + ) + + returnedInfos := make(map[string]*info.ContainerInfo, len(containers)) + + for _, container := range containers { + // nil should give us default values + cinfo, err := m.GetContainerInfo(container, nil) + if err != nil { + t.Fatalf("Unable to get info for container %v: %v", container, err) + } + returnedInfos[container] = cinfo + } + + for container, handler := range handlerMap { + handler.AssertExpectations(t) + returned := returnedInfos[container] + expected := infosMap[container] + if !reflect.DeepEqual(returned, expected) { + t.Errorf("returned unexpected info for container %v; returned %+v; expected %+v", container, returned, expected) + } + } + +} diff --git a/third_party/src/github.com/google/cadvisor/pages/containers.go b/third_party/src/github.com/google/cadvisor/pages/containers.go index 58605a8bc8e..b3302b171e6 100644 --- a/third_party/src/github.com/google/cadvisor/pages/containers.go +++ b/third_party/src/github.com/google/cadvisor/pages/containers.go @@ -162,7 +162,11 @@ func ServerContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL) containerName := u.Path[len(ContainersPage)-1:] // Get the container. - cont, err := m.GetContainerInfo(containerName) + reqParams := info.ContainerInfoRequest{ + NumStats: 60, + NumSamples: 60, + } + cont, err := m.GetContainerInfo(containerName, &reqParams) if err != nil { return fmt.Errorf("Failed to get container \"%s\" with error: %s", containerName, err) } diff --git a/third_party/src/github.com/google/cadvisor/pages/static/containers_js.go b/third_party/src/github.com/google/cadvisor/pages/static/containers_js.go index b907b344d65..086c610b8f9 100644 --- a/third_party/src/github.com/google/cadvisor/pages/static/containers_js.go +++ b/third_party/src/github.com/google/cadvisor/pages/static/containers_js.go @@ -128,7 +128,7 @@ function drawCpuPerCoreUsage(elementId, machineInfo, stats) { elements.push(cur.timestamp); for (var j = 0; j < machineInfo.num_cores; j++) { // TODO(vmarmol): This assumes we sample every second, use the timestamps. - elements.push((cur.cpu.usage.per_cpu[j] - prev.cpu.usage.per_cpu[j]) / 1000000000); + elements.push((cur.cpu.usage.per_cpu_usage[j] - prev.cpu.usage.per_cpu_usage[j]) / 1000000000); } data.push(elements); } diff --git a/third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb.go b/third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb.go new file mode 100644 index 00000000000..99cb92c718f --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb.go @@ -0,0 +1,498 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package influxdb + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/google/cadvisor/info" + "github.com/google/cadvisor/storage" + "github.com/influxdb/influxdb-go" +) + +type influxdbStorage struct { + client *influxdb.Client + prevStats *info.ContainerStats + machineName string + tableName string + windowLen time.Duration +} + +const ( + colTimestamp string = "timestamp" + colMachineName string = "machine" + colContainerName string = "container_name" + colCpuCumulativeUsage string = "cpu_cumulative_usage" + // Cumulative Cpu Usage in system mode + colCpuCumulativeUsageSystem string = "cpu_cumulative_usage_system" + // Cumulative Cpu Usage in user mode + colCpuCumulativeUsageUser string = "cpu_cumulative_usage_user" + // Memory Usage + colMemoryUsage string = "memory_usage" + // Working set size + colMemoryWorkingSet string = "memory_working_set" + // container page fault + colMemoryContainerPgfault string = "memory_container_pgfault" + // container major page fault + colMemoryContainerPgmajfault string = "memory_container_pgmajfault" + // hierarchical page fault + colMemoryHierarchicalPgfault string = "memory_hierarchical_pgfault" + // hierarchical major page fault + colMemoryHierarchicalPgmajfault string = "memory_hierarchical_pgmajfault" + // Cumulative per core usage + colPerCoreCumulativeUsagePrefix string = "per_core_cumulative_usage_core_" + // Optional: sample duration. Unit: Nanosecond. + colSampleDuration string = "sample_duration" + // Optional: Instant cpu usage + colCpuInstantUsage string = "cpu_instant_usage" + // Optional: Instant per core usage + colPerCoreInstantUsagePrefix string = "per_core_instant_usage_core_" +) + +func (self *influxdbStorage) containerStatsToValues( + ref info.ContainerReference, + stats *info.ContainerStats, +) (columns []string, values []interface{}) { + + // Timestamp + columns = append(columns, colTimestamp) + values = append(values, stats.Timestamp.Format(time.RFC3339Nano)) + + // Machine name + columns = append(columns, colMachineName) + values = append(values, self.machineName) + + // Container name + columns = append(columns, colContainerName) + values = append(values, ref.Name) + + // Cumulative Cpu Usage + columns = append(columns, colCpuCumulativeUsage) + values = append(values, stats.Cpu.Usage.Total) + + // Cumulative Cpu Usage in system mode + columns = append(columns, colCpuCumulativeUsageSystem) + values = append(values, stats.Cpu.Usage.System) + + // Cumulative Cpu Usage in user mode + columns = append(columns, colCpuCumulativeUsageUser) + values = append(values, stats.Cpu.Usage.User) + + // Memory Usage + columns = append(columns, colMemoryUsage) + values = append(values, stats.Memory.Usage) + + // Working set size + columns = append(columns, colMemoryWorkingSet) + values = append(values, stats.Memory.WorkingSet) + + // container page fault + columns = append(columns, colMemoryContainerPgfault) + values = append(values, stats.Memory.ContainerData.Pgfault) + + // container major page fault + columns = append(columns, colMemoryContainerPgmajfault) + values = append(values, stats.Memory.ContainerData.Pgmajfault) + + // hierarchical page fault + columns = append(columns, colMemoryHierarchicalPgfault) + values = append(values, stats.Memory.HierarchicalData.Pgfault) + + // hierarchical major page fault + columns = append(columns, colMemoryHierarchicalPgmajfault) + values = append(values, stats.Memory.HierarchicalData.Pgmajfault) + + // per cpu cumulative usage + for i, u := range stats.Cpu.Usage.PerCpu { + columns = append(columns, fmt.Sprintf("%v%v", colPerCoreCumulativeUsagePrefix, i)) + values = append(values, u) + } + + sample, err := info.NewSample(self.prevStats, stats) + if err != nil || sample == nil { + return columns, values + } + + // Optional: sample duration. Unit: Nanosecond. + columns = append(columns, colSampleDuration) + values = append(values, sample.Duration.String()) + + // Optional: Instant cpu usage + columns = append(columns, colCpuInstantUsage) + values = append(values, sample.Cpu.Usage) + + // Optional: Instant per core usage + for i, u := range sample.Cpu.PerCpuUsage { + columns = append(columns, fmt.Sprintf("%v%v", colPerCoreInstantUsagePrefix, i)) + values = append(values, u) + } + + return columns, values +} + +func convertToUint64(v interface{}) (uint64, error) { + if v == nil { + return 0, nil + } + switch x := v.(type) { + case uint64: + return x, nil + case int: + if x < 0 { + return 0, fmt.Errorf("negative value: %v", x) + } + return uint64(x), nil + case int32: + if x < 0 { + return 0, fmt.Errorf("negative value: %v", x) + } + return uint64(x), nil + case int64: + if x < 0 { + return 0, fmt.Errorf("negative value: %v", x) + } + return uint64(x), nil + case float64: + if x < 0 { + return 0, fmt.Errorf("negative value: %v", x) + } + return uint64(x), nil + case uint32: + return uint64(x), nil + } + return 0, fmt.Errorf("Unknown type") +} + +func (self *influxdbStorage) valuesToContainerStats(columns []string, values []interface{}) (*info.ContainerStats, error) { + stats := &info.ContainerStats{ + Cpu: &info.CpuStats{}, + Memory: &info.MemoryStats{}, + } + perCoreUsage := make(map[int]uint64, 32) + var err error + for i, col := range columns { + v := values[i] + switch { + case col == colTimestamp: + if str, ok := v.(string); ok { + stats.Timestamp, err = time.Parse(time.RFC3339Nano, str) + } + case col == colMachineName: + if m, ok := v.(string); ok { + if m != self.machineName { + return nil, fmt.Errorf("different machine") + } + } else { + return nil, fmt.Errorf("machine name field is not a string: %v", v) + } + // Cumulative Cpu Usage + case col == colCpuCumulativeUsage: + stats.Cpu.Usage.Total, err = convertToUint64(v) + // Cumulative Cpu used by the system + case col == colCpuCumulativeUsageSystem: + stats.Cpu.Usage.System, err = convertToUint64(v) + // Cumulative Cpu Usage in user mode + case col == colCpuCumulativeUsageUser: + stats.Cpu.Usage.User, err = convertToUint64(v) + // Memory Usage + case col == colMemoryUsage: + stats.Memory.Usage, err = convertToUint64(v) + // Working set size + case col == colMemoryWorkingSet: + stats.Memory.WorkingSet, err = convertToUint64(v) + // container page fault + case col == colMemoryContainerPgfault: + stats.Memory.ContainerData.Pgfault, err = convertToUint64(v) + // container major page fault + case col == colMemoryContainerPgmajfault: + stats.Memory.ContainerData.Pgmajfault, err = convertToUint64(v) + // hierarchical page fault + case col == colMemoryHierarchicalPgfault: + stats.Memory.HierarchicalData.Pgfault, err = convertToUint64(v) + // hierarchical major page fault + case col == colMemoryHierarchicalPgmajfault: + stats.Memory.HierarchicalData.Pgmajfault, err = convertToUint64(v) + case strings.HasPrefix(col, colPerCoreCumulativeUsagePrefix): + idxStr := col[len(colPerCoreCumulativeUsagePrefix):] + idx, err := strconv.Atoi(idxStr) + if err != nil { + continue + } + perCoreUsage[idx], err = convertToUint64(v) + } + if err != nil { + return nil, fmt.Errorf("column %v has invalid value %v: %v", col, v, err) + } + } + stats.Cpu.Usage.PerCpu = make([]uint64, len(perCoreUsage)) + for idx, usage := range perCoreUsage { + stats.Cpu.Usage.PerCpu[idx] = usage + } + return stats, nil +} + +func (self *influxdbStorage) valuesToContainerSample(columns []string, values []interface{}) (*info.ContainerStatsSample, error) { + sample := &info.ContainerStatsSample{} + perCoreUsage := make(map[int]uint64, 32) + var err error + for i, col := range columns { + v := values[i] + switch { + case col == colTimestamp: + if str, ok := v.(string); ok { + sample.Timestamp, err = time.Parse(time.RFC3339Nano, str) + } + case col == colMachineName: + if m, ok := v.(string); ok { + if m != self.machineName { + return nil, fmt.Errorf("different machine") + } + } else { + return nil, fmt.Errorf("machine name field is not a string: %v", v) + } + // Memory Usage + case col == colMemoryUsage: + sample.Memory.Usage, err = convertToUint64(v) + // sample duration. Unit: Nanosecond. + case col == colSampleDuration: + if v == nil { + // this record does not have sample_duration, so it's the first stats. + return nil, nil + } + sample.Duration, err = time.ParseDuration(v.(string)) + // Instant cpu usage + case col == colCpuInstantUsage: + sample.Cpu.Usage, err = convertToUint64(v) + case strings.HasPrefix(col, colPerCoreInstantUsagePrefix): + idxStr := col[len(colPerCoreInstantUsagePrefix):] + idx, err := strconv.Atoi(idxStr) + if err != nil { + continue + } + perCoreUsage[idx], err = convertToUint64(v) + } + if err != nil { + return nil, fmt.Errorf("column %v has invalid value %v: %v", col, v, err) + } + } + sample.Cpu.PerCpuUsage = make([]uint64, len(perCoreUsage)) + for idx, usage := range perCoreUsage { + sample.Cpu.PerCpuUsage[idx] = usage + } + if sample.Duration.Nanoseconds() == 0 { + return nil, nil + } + return sample, nil +} + +func (self *influxdbStorage) AddStats(ref info.ContainerReference, stats *info.ContainerStats) error { + series := &influxdb.Series{ + Name: self.tableName, + // There's only one point for each stats + Points: make([][]interface{}, 1), + } + if stats == nil || stats.Cpu == nil || stats.Memory == nil { + return nil + } + series.Columns, series.Points[0] = self.containerStatsToValues(ref, stats) + + self.prevStats = stats.Copy(self.prevStats) + err := self.client.WriteSeries([]*influxdb.Series{series}) + if err != nil { + return err + } + return nil +} + +func (self *influxdbStorage) RecentStats(containerName string, numStats int) ([]*info.ContainerStats, error) { + // TODO(dengnan): select only columns that we need + // TODO(dengnan): escape names + query := fmt.Sprintf("select * from %v where %v='%v' and %v='%v'", self.tableName, colContainerName, containerName, colMachineName, self.machineName) + if numStats > 0 { + query = fmt.Sprintf("%v limit %v", query, numStats) + } + series, err := self.client.Query(query) + if err != nil { + return nil, err + } + statsList := make([]*info.ContainerStats, 0, len(series)) + // By default, influxDB returns data in time descending order. + // RecentStats() requires stats in time increasing order, + // so we need to go through from the last one to the first one. + for i := len(series) - 1; i >= 0; i-- { + s := series[i] + for j := len(s.Points) - 1; j >= 0; j-- { + values := s.Points[j] + stats, err := self.valuesToContainerStats(s.Columns, values) + if err != nil { + return nil, err + } + if stats == nil { + continue + } + statsList = append(statsList, stats) + } + } + return statsList, nil +} + +func (self *influxdbStorage) Samples(containerName string, numSamples int) ([]*info.ContainerStatsSample, error) { + // TODO(dengnan): select only columns that we need + // TODO(dengnan): escape names + query := fmt.Sprintf("select * from %v where %v='%v' and %v='%v'", self.tableName, colContainerName, containerName, colMachineName, self.machineName) + if numSamples > 0 { + query = fmt.Sprintf("%v limit %v", query, numSamples) + } + series, err := self.client.Query(query) + if err != nil { + return nil, err + } + sampleList := make([]*info.ContainerStatsSample, 0, len(series)) + for i := len(series) - 1; i >= 0; i-- { + s := series[i] + for j := len(s.Points) - 1; j >= 0; j-- { + values := s.Points[j] + sample, err := self.valuesToContainerSample(s.Columns, values) + if err != nil { + return nil, err + } + if sample == nil { + continue + } + sampleList = append(sampleList, sample) + } + } + return sampleList, nil +} + +func (self *influxdbStorage) Close() error { + self.client = nil + return nil +} + +func (self *influxdbStorage) Percentiles( + containerName string, + cpuUsagePercentiles []int, + memUsagePercentiles []int, +) (*info.ContainerStatsPercentiles, error) { + selectedCol := make([]string, 0, len(cpuUsagePercentiles)+len(memUsagePercentiles)+1) + + selectedCol = append(selectedCol, fmt.Sprintf("max(%v)", colMemoryUsage)) + for _, p := range cpuUsagePercentiles { + selectedCol = append(selectedCol, fmt.Sprintf("percentile(%v, %v)", colCpuInstantUsage, p)) + } + for _, p := range memUsagePercentiles { + selectedCol = append(selectedCol, fmt.Sprintf("percentile(%v, %v)", colMemoryUsage, p)) + } + + query := fmt.Sprintf("select %v from %v where %v='%v' and %v='%v' and time > now() - %v", + strings.Join(selectedCol, ","), + self.tableName, + colContainerName, + containerName, + colMachineName, + self.machineName, + fmt.Sprintf("%vs", self.windowLen.Seconds()), + ) + series, err := self.client.Query(query) + if err != nil { + return nil, err + } + if len(series) != 1 { + return nil, nil + } + if len(series[0].Points) == 0 { + return nil, nil + } + + point := series[0].Points[0] + + ret := new(info.ContainerStatsPercentiles) + ret.MaxMemoryUsage, err = convertToUint64(point[1]) + if err != nil { + return nil, fmt.Errorf("invalid max memory usage: %v", err) + } + retrievedCpuPercentiles := point[2 : 2+len(cpuUsagePercentiles)] + for i, p := range cpuUsagePercentiles { + v, err := convertToUint64(retrievedCpuPercentiles[i]) + if err != nil { + return nil, fmt.Errorf("invalid cpu usage: %v", err) + } + ret.CpuUsagePercentiles = append( + ret.CpuUsagePercentiles, + info.Percentile{ + Percentage: p, + Value: v, + }, + ) + } + retrievedMemoryPercentiles := point[2+len(cpuUsagePercentiles):] + for i, p := range memUsagePercentiles { + v, err := convertToUint64(retrievedMemoryPercentiles[i]) + if err != nil { + return nil, fmt.Errorf("invalid memory usage: %v", err) + } + ret.MemoryUsagePercentiles = append( + ret.MemoryUsagePercentiles, + info.Percentile{ + Percentage: p, + Value: v, + }, + ) + } + return ret, nil +} + +// machineName: A unique identifier to identify the host that current cAdvisor +// instance is running on. +// influxdbHost: The host which runs influxdb. +// percentilesDuration: Time window which will be considered when calls Percentiles() +func New(machineName, + tablename, + database, + username, + password, + influxdbHost string, + isSecure bool, + percentilesDuration time.Duration, +) (storage.StorageDriver, error) { + config := &influxdb.ClientConfig{ + Host: influxdbHost, + Username: username, + Password: password, + Database: database, + IsSecure: isSecure, + } + client, err := influxdb.NewClient(config) + if err != nil { + return nil, err + } + // TODO(monnand): With go 1.3, we cannot compress data now. + client.DisableCompression() + if percentilesDuration.Seconds() < 1.0 { + percentilesDuration = 5 * time.Minute + } + + ret := &influxdbStorage{ + client: client, + windowLen: percentilesDuration, + machineName: machineName, + tableName: tablename, + } + return ret, nil +} diff --git a/third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb_test.go b/third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb_test.go new file mode 100644 index 00000000000..c5770d35d1b --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/storage/influxdb/influxdb_test.go @@ -0,0 +1,136 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package influxdb + +import ( + "fmt" + "testing" + "time" + + "github.com/google/cadvisor/storage" + "github.com/google/cadvisor/storage/test" + "github.com/influxdb/influxdb-go" +) + +func runStorageTest(f func(storage.StorageDriver, *testing.T), t *testing.T) { + machineName := "machineA" + tablename := "t" + database := "cadvisor" + username := "root" + password := "root" + hostname := "localhost:8086" + percentilesDuration := 10 * time.Minute + rootConfig := &influxdb.ClientConfig{ + Host: hostname, + Username: username, + Password: password, + IsSecure: false, + } + rootClient, err := influxdb.NewClient(rootConfig) + if err != nil { + t.Fatal(err) + } + // create the data base first. + rootClient.CreateDatabase(database) + config := &influxdb.ClientConfig{ + Host: hostname, + Username: username, + Password: password, + Database: database, + IsSecure: false, + } + client, err := influxdb.NewClient(config) + if err != nil { + t.Fatal(err) + } + client.DisableCompression() + deleteAll := fmt.Sprintf("drop series %v", tablename) + _, err = client.Query(deleteAll) + if err != nil { + t.Fatal(err) + } + // delete all data by the end of the call + defer client.Query(deleteAll) + + driver, err := New(machineName, + tablename, + database, + username, + password, + hostname, + false, + percentilesDuration) + if err != nil { + t.Fatal(err) + } + // generate another container's data on same machine. + test.StorageDriverFillRandomStatsFunc("containerOnSameMachine", 100, driver, t) + + // generate another container's data on another machine. + driverForAnotherMachine, err := New("machineB", + tablename, + database, + username, + password, + hostname, + false, + percentilesDuration) + if err != nil { + t.Fatal(err) + } + defer driverForAnotherMachine.Close() + test.StorageDriverFillRandomStatsFunc("containerOnAnotherMachine", 100, driverForAnotherMachine, t) + f(driver, t) +} + +func TestSampleCpuUsage(t *testing.T) { + runStorageTest(test.StorageDriverTestSampleCpuUsage, t) +} + +func TestRetrievePartialRecentStats(t *testing.T) { + runStorageTest(test.StorageDriverTestRetrievePartialRecentStats, t) +} + +func TestSamplesWithoutSample(t *testing.T) { + runStorageTest(test.StorageDriverTestSamplesWithoutSample, t) +} + +func TestRetrieveAllRecentStats(t *testing.T) { + runStorageTest(test.StorageDriverTestRetrieveAllRecentStats, t) +} + +func TestNoRecentStats(t *testing.T) { + runStorageTest(test.StorageDriverTestNoRecentStats, t) +} + +func TestNoSamples(t *testing.T) { + runStorageTest(test.StorageDriverTestNoSamples, t) +} + +func TestPercentiles(t *testing.T) { + runStorageTest(test.StorageDriverTestPercentiles, t) +} + +func TestMaxMemoryUsage(t *testing.T) { + runStorageTest(test.StorageDriverTestMaxMemoryUsage, t) +} + +func TestPercentilesWithoutSample(t *testing.T) { + runStorageTest(test.StorageDriverTestPercentilesWithoutSample, t) +} + +func TestPercentilesWithoutStats(t *testing.T) { + runStorageTest(test.StorageDriverTestPercentilesWithoutStats, t) +} diff --git a/third_party/src/github.com/google/cadvisor/storage/memory/memory.go b/third_party/src/github.com/google/cadvisor/storage/memory/memory.go index dd353cc4634..d30a3df62c4 100644 --- a/third_party/src/github.com/google/cadvisor/storage/memory/memory.go +++ b/third_party/src/github.com/google/cadvisor/storage/memory/memory.go @@ -41,20 +41,7 @@ func (self *containerStorage) updatePrevStats(stats *info.ContainerStats) { self.prevStats = nil return } - if self.prevStats == nil { - self.prevStats = &info.ContainerStats{ - Cpu: &info.CpuStats{}, - Memory: &info.MemoryStats{}, - } - } - // make a deep copy. - self.prevStats.Timestamp = stats.Timestamp - *self.prevStats.Cpu = *stats.Cpu - self.prevStats.Cpu.Usage.PerCpu = make([]uint64, len(stats.Cpu.Usage.PerCpu)) - for i, perCpu := range stats.Cpu.Usage.PerCpu { - self.prevStats.Cpu.Usage.PerCpu[i] = perCpu - } - *self.prevStats.Memory = *stats.Memory + self.prevStats = stats.Copy(self.prevStats) } func (self *containerStorage) AddStats(stats *info.ContainerStats) error { @@ -75,9 +62,9 @@ func (self *containerStorage) AddStats(stats *info.ContainerStats) error { } } if self.recentStats.Len() >= self.maxNumStats { - self.recentStats.Remove(self.recentStats.Front()) + self.recentStats.Remove(self.recentStats.Back()) } - self.recentStats.PushBack(stats) + self.recentStats.PushFront(stats) self.updatePrevStats(stats) return nil } @@ -88,18 +75,25 @@ func (self *containerStorage) RecentStats(numStats int) ([]*info.ContainerStats, if self.recentStats.Len() < numStats || numStats < 0 { numStats = self.recentStats.Len() } - ret := make([]*info.ContainerStats, 0, numStats) + + // Stats in the recentStats list are stored in reverse chronological + // order, i.e. most recent stats is in the front. + // numStats will always <= recentStats.Len() so that there will be + // always at least numStats available stats to retrieve. We traverse + // the recentStats list from its head and fill the ret slice in + // reverse order so that the returned slice will be in chronological + // order. The order of the returned slice is not specified by the + // StorageDriver interface, so it is not necessary for other storage + // drivers to return the slice in the same order. + ret := make([]*info.ContainerStats, numStats) e := self.recentStats.Front() - for i := 0; i < numStats; i++ { + for i := numStats - 1; i >= 0; i-- { data, ok := e.Value.(*info.ContainerStats) if !ok { return nil, fmt.Errorf("The %vth element is not a ContainerStats", i) } - ret = append(ret, data) + ret[i] = data e = e.Next() - if e == nil { - break - } } return ret, nil } @@ -162,23 +156,34 @@ type InMemoryStorage struct { func (self *InMemoryStorage) AddStats(ref info.ContainerReference, stats *info.ContainerStats) error { var cstore *containerStorage var ok bool - self.lock.Lock() - if cstore, ok = self.containerStorageMap[ref.Name]; !ok { - cstore = newContainerStore(ref, self.maxNumSamples, self.maxNumStats) - self.containerStorageMap[ref.Name] = cstore - } - self.lock.Unlock() + + func() { + self.lock.Lock() + defer self.lock.Unlock() + if cstore, ok = self.containerStorageMap[ref.Name]; !ok { + cstore = newContainerStore(ref, self.maxNumSamples, self.maxNumStats) + self.containerStorageMap[ref.Name] = cstore + } + }() return cstore.AddStats(stats) } func (self *InMemoryStorage) Samples(name string, numSamples int) ([]*info.ContainerStatsSample, error) { var cstore *containerStorage var ok bool - self.lock.RLock() - if cstore, ok = self.containerStorageMap[name]; !ok { - return nil, fmt.Errorf("unable to find data for container %v", name) + + err := func() error { + self.lock.RLock() + defer self.lock.RUnlock() + if cstore, ok = self.containerStorageMap[name]; !ok { + return fmt.Errorf("unable to find data for container %v", name) + } + return nil + }() + + if err != nil { + return nil, err } - self.lock.RUnlock() return cstore.Samples(numSamples) } @@ -186,11 +191,17 @@ func (self *InMemoryStorage) Samples(name string, numSamples int) ([]*info.Conta func (self *InMemoryStorage) RecentStats(name string, numStats int) ([]*info.ContainerStats, error) { var cstore *containerStorage var ok bool - self.lock.RLock() - if cstore, ok = self.containerStorageMap[name]; !ok { - return nil, fmt.Errorf("unable to find data for container %v", name) + err := func() error { + self.lock.RLock() + defer self.lock.RUnlock() + if cstore, ok = self.containerStorageMap[name]; !ok { + return fmt.Errorf("unable to find data for container %v", name) + } + return nil + }() + if err != nil { + return nil, err } - self.lock.RUnlock() return cstore.RecentStats(numStats) } @@ -198,11 +209,17 @@ func (self *InMemoryStorage) RecentStats(name string, numStats int) ([]*info.Con func (self *InMemoryStorage) Percentiles(name string, cpuPercentiles, memPercentiles []int) (*info.ContainerStatsPercentiles, error) { var cstore *containerStorage var ok bool - self.lock.RLock() - if cstore, ok = self.containerStorageMap[name]; !ok { - return nil, fmt.Errorf("unable to find data for container %v", name) + err := func() error { + self.lock.RLock() + defer self.lock.RUnlock() + if cstore, ok = self.containerStorageMap[name]; !ok { + return fmt.Errorf("unable to find data for container %v", name) + } + return nil + }() + if err != nil { + return nil, err } - self.lock.RUnlock() return cstore.Percentiles(cpuPercentiles, memPercentiles) } diff --git a/third_party/src/github.com/google/cadvisor/storage/memory/memory_test.go b/third_party/src/github.com/google/cadvisor/storage/memory/memory_test.go index 98f9a6a8810..394e48f6a5f 100644 --- a/third_party/src/github.com/google/cadvisor/storage/memory/memory_test.go +++ b/third_party/src/github.com/google/cadvisor/storage/memory/memory_test.go @@ -44,6 +44,32 @@ func TestSamplesWithoutSample(t *testing.T) { runStorageTest(test.StorageDriverTestSamplesWithoutSample, t) } -func TestPercentilessWithoutSample(t *testing.T) { +func TestPercentilesWithoutSample(t *testing.T) { runStorageTest(test.StorageDriverTestPercentilesWithoutSample, t) } + +func TestPercentiles(t *testing.T) { + N := 100 + driver := New(N, N) + test.StorageDriverTestPercentiles(driver, t) +} + +func TestRetrievePartialRecentStats(t *testing.T) { + runStorageTest(test.StorageDriverTestRetrievePartialRecentStats, t) +} + +func TestRetrieveAllRecentStats(t *testing.T) { + runStorageTest(test.StorageDriverTestRetrieveAllRecentStats, t) +} + +func TestNoRecentStats(t *testing.T) { + runStorageTest(test.StorageDriverTestNoRecentStats, t) +} + +func TestNoSamples(t *testing.T) { + runStorageTest(test.StorageDriverTestNoSamples, t) +} + +func TestPercentilesWithoutStats(t *testing.T) { + runStorageTest(test.StorageDriverTestPercentilesWithoutStats, t) +} diff --git a/third_party/src/github.com/google/cadvisor/storage/storage.go b/third_party/src/github.com/google/cadvisor/storage/storage.go index 51f0b9c00f5..84ca0b3b610 100644 --- a/third_party/src/github.com/google/cadvisor/storage/storage.go +++ b/third_party/src/github.com/google/cadvisor/storage/storage.go @@ -21,7 +21,9 @@ type StorageDriver interface { // Read most recent stats. numStats indicates max number of stats // returned. The returned stats must be consecutive observed stats. If - // numStats < 0, then return all stats stored in the storage. + // numStats < 0, then return all stats stored in the storage. The + // returned stats should be sorted in time increasing order, i.e. Most + // recent stats should be the last. RecentStats(containerName string, numStats int) ([]*info.ContainerStats, error) // Read the specified percentiles of CPU and memory usage of the container. @@ -31,7 +33,7 @@ type StorageDriver interface { // Returns samples of the container stats. If numSamples < 0, then // the number of returned samples is implementation defined. Otherwise, the driver // should return at most numSamples samples. - Samples(containername string, numSamples int) ([]*info.ContainerStatsSample, error) + Samples(containerName string, numSamples int) ([]*info.ContainerStatsSample, error) // Close will clear the state of the storage driver. The elements // stored in the underlying storage may or may not be deleted depending diff --git a/third_party/src/github.com/google/cadvisor/storage/test/mock.go b/third_party/src/github.com/google/cadvisor/storage/test/mock.go new file mode 100644 index 00000000000..24fe2500948 --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/storage/test/mock.go @@ -0,0 +1,57 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "github.com/google/cadvisor/info" + "github.com/stretchr/testify/mock" +) + +type MockStorageDriver struct { + mock.Mock + MockCloseMethod bool +} + +func (self *MockStorageDriver) AddStats(ref info.ContainerReference, stats *info.ContainerStats) error { + args := self.Called(ref, stats) + return args.Error(0) +} + +func (self *MockStorageDriver) RecentStats(containerName string, numStats int) ([]*info.ContainerStats, error) { + args := self.Called(containerName, numStats) + return args.Get(0).([]*info.ContainerStats), args.Error(1) +} + +func (self *MockStorageDriver) Percentiles( + containerName string, + cpuUsagePercentiles []int, + memUsagePercentiles []int, +) (*info.ContainerStatsPercentiles, error) { + args := self.Called(containerName, cpuUsagePercentiles, memUsagePercentiles) + return args.Get(0).(*info.ContainerStatsPercentiles), args.Error(1) +} + +func (self *MockStorageDriver) Samples(containerName string, numSamples int) ([]*info.ContainerStatsSample, error) { + args := self.Called(containerName, numSamples) + return args.Get(0).([]*info.ContainerStatsSample), args.Error(1) +} + +func (self *MockStorageDriver) Close() error { + if self.MockCloseMethod { + args := self.Called() + return args.Error(0) + } + return nil +} diff --git a/third_party/src/github.com/google/cadvisor/storage/test/storagetests.go b/third_party/src/github.com/google/cadvisor/storage/test/storagetests.go index e0f27f76aa7..8bad2c4d7a9 100644 --- a/third_party/src/github.com/google/cadvisor/storage/test/storagetests.go +++ b/third_party/src/github.com/google/cadvisor/storage/test/storagetests.go @@ -16,6 +16,7 @@ package test import ( "math/rand" + "reflect" "testing" "time" @@ -43,6 +44,7 @@ func buildTrace(cpu, mem []uint64, duration time.Duration) []*info.ContainerStat stats.Cpu.Usage.Total = cpuTotalUsage stats.Cpu.Usage.User = stats.Cpu.Usage.Total stats.Cpu.Usage.System = 0 + stats.Cpu.Usage.PerCpu = []uint64{cpuTotalUsage} stats.Memory.Usage = mem[i] @@ -51,10 +53,133 @@ func buildTrace(cpu, mem []uint64, duration time.Duration) []*info.ContainerStat return ret } -// The underlying driver must be able to hold more than 10 samples. +func timeEq(t1, t2 time.Time, tolerance time.Duration) bool { + // t1 should not be later than t2 + if t1.After(t2) { + t1, t2 = t2, t1 + } + diff := t2.Sub(t1) + if diff <= tolerance { + return true + } + return false +} + +func durationEq(a, b time.Duration, tolerance time.Duration) bool { + if a > b { + a, b = b, a + } + diff := a - b + if diff <= tolerance { + return true + } + return false +} + +const ( + // 10ms, i.e. 0.01s + timePrecision time.Duration = 10 * time.Millisecond +) + +// This function is useful because we do not require precise time +// representation. +func statsEq(a, b *info.ContainerStats) bool { + if !timeEq(a.Timestamp, b.Timestamp, timePrecision) { + return false + } + if !reflect.DeepEqual(a.Cpu, b.Cpu) { + return false + } + if !reflect.DeepEqual(a.Memory, b.Memory) { + return false + } + return true +} + +// This function is useful because we do not require precise time +// representation. +func sampleEq(a, b *info.ContainerStatsSample) bool { + if !timeEq(a.Timestamp, b.Timestamp, timePrecision) { + return false + } + if !durationEq(a.Duration, b.Duration, timePrecision) { + return false + } + if !reflect.DeepEqual(a.Cpu, b.Cpu) { + return false + } + if !reflect.DeepEqual(a.Memory, b.Memory) { + return false + } + return true +} + +func samplesInTrace(samples []*info.ContainerStatsSample, cpuTrace, memTrace []uint64, samplePeriod time.Duration, t *testing.T) { + for _, sample := range samples { + if sample.Duration != samplePeriod { + t.Errorf("sample duration is %v, not %v", sample.Duration, samplePeriod) + } + cpuUsage := sample.Cpu.Usage + memUsage := sample.Memory.Usage + found := false + for _, u := range cpuTrace { + if u == cpuUsage { + found = true + break + } + } + if !found { + t.Errorf("unable to find cpu usage %v", cpuUsage) + } + found = false + for _, u := range memTrace { + if u == memUsage { + found = true + break + } + } + if !found { + t.Errorf("unable to find mem usage %v", memUsage) + } + } +} + +// This function will generate random stats and write +// them into the storage. The function will not close the driver +func StorageDriverFillRandomStatsFunc( + containerName string, + N int, + driver storage.StorageDriver, + t *testing.T, +) { + cpuTrace := make([]uint64, 0, N) + memTrace := make([]uint64, 0, N) + + // We need N+1 observations to get N samples + for i := 0; i < N+1; i++ { + cpuTrace = append(cpuTrace, uint64(rand.Intn(1000))) + memTrace = append(memTrace, uint64(rand.Intn(1000))) + } + + samplePeriod := 1 * time.Second + + ref := info.ContainerReference{ + Name: containerName, + } + + trace := buildTrace(cpuTrace, memTrace, samplePeriod) + + for _, stats := range trace { + err := driver.AddStats(ref, stats) + if err != nil { + t.Fatalf("unable to add stats: %v", err) + } + } +} + func StorageDriverTestSampleCpuUsage(driver storage.StorageDriver, t *testing.T) { defer driver.Close() - N := 10 + N := 100 cpuTrace := make([]uint64, 0, N) memTrace := make([]uint64, 0, N) @@ -73,28 +198,37 @@ func StorageDriverTestSampleCpuUsage(driver storage.StorageDriver, t *testing.T) trace := buildTrace(cpuTrace, memTrace, samplePeriod) for _, stats := range trace { - driver.AddStats(ref, stats) + err := driver.AddStats(ref, stats) + if err != nil { + t.Fatalf("unable to add stats: %v", err) + } + // set the trace to something else. The stats stored in the + // storage should not be affected. + stats.Cpu.Usage.Total = 0 + stats.Cpu.Usage.System = 0 + stats.Cpu.Usage.User = 0 } samples, err := driver.Samples(ref.Name, N) if err != nil { t.Errorf("unable to sample stats: %v", err) } - for _, sample := range samples { - if sample.Duration != samplePeriod { - t.Errorf("sample duration is %v, not %v", sample.Duration, samplePeriod) - } - cpuUsage := sample.Cpu.Usage - found := false - for _, u := range cpuTrace { - if u == cpuUsage { - found = true - } - } - if !found { - t.Errorf("unable to find cpu usage %v", cpuUsage) - } + if len(samples) == 0 { + t.Fatal("should at least store one sample") } + samplesInTrace(samples, cpuTrace, memTrace, samplePeriod, t) + + samples, err = driver.Samples(ref.Name, -1) + if err != nil { + t.Errorf("unable to sample stats: %v", err) + } + samplesInTrace(samples, cpuTrace, memTrace, samplePeriod, t) + + samples, err = driver.Samples(ref.Name, N-5) + if err != nil { + t.Errorf("unable to sample stats: %v", err) + } + samplesInTrace(samples, cpuTrace, memTrace, samplePeriod, t) } func StorageDriverTestMaxMemoryUsage(driver storage.StorageDriver, t *testing.T) { @@ -114,7 +248,16 @@ func StorageDriverTestMaxMemoryUsage(driver storage.StorageDriver, t *testing.T) trace := buildTrace(cpuTrace, memTrace, 1*time.Second) for _, stats := range trace { - driver.AddStats(ref, stats) + err := driver.AddStats(ref, stats) + if err != nil { + t.Fatalf("unable to add stats: %v", err) + } + // set the trace to something else. The stats stored in the + // storage should not be affected. + stats.Cpu.Usage.Total = 0 + stats.Cpu.Usage.System = 0 + stats.Cpu.Usage.User = 0 + stats.Memory.Usage = 0 } percentiles, err := driver.Percentiles(ref.Name, []int{50}, []int{50}) @@ -168,3 +311,174 @@ func StorageDriverTestPercentilesWithoutSample(driver storage.StorageDriver, t * t.Errorf("There should be no percentiles") } } + +func StorageDriverTestPercentiles(driver storage.StorageDriver, t *testing.T) { + defer driver.Close() + N := 100 + cpuTrace := make([]uint64, N) + memTrace := make([]uint64, N) + for i := 1; i < N+1; i++ { + cpuTrace[i-1] = uint64(i) + memTrace[i-1] = uint64(i) + } + + trace := buildTrace(cpuTrace, memTrace, 1*time.Second) + + ref := info.ContainerReference{ + Name: "container", + } + for _, stats := range trace { + driver.AddStats(ref, stats) + } + percentages := []int{ + 80, + 90, + 50, + } + percentiles, err := driver.Percentiles(ref.Name, percentages, percentages) + if err != nil { + t.Fatal(err) + } + for _, x := range percentiles.CpuUsagePercentiles { + for _, y := range percentiles.CpuUsagePercentiles { + // lower percentage, smaller value + if x.Percentage < y.Percentage && x.Value > y.Value { + t.Errorf("%v percent is %v; while %v percent is %v", + x.Percentage, x.Value, y.Percentage, y.Value) + } + } + } + for _, x := range percentiles.MemoryUsagePercentiles { + for _, y := range percentiles.MemoryUsagePercentiles { + if x.Percentage < y.Percentage && x.Value > y.Value { + t.Errorf("%v percent is %v; while %v percent is %v", + x.Percentage, x.Value, y.Percentage, y.Value) + } + } + } +} + +func StorageDriverTestRetrievePartialRecentStats(driver storage.StorageDriver, t *testing.T) { + defer driver.Close() + N := 100 + memTrace := make([]uint64, N) + cpuTrace := make([]uint64, N) + for i := 0; i < N; i++ { + memTrace[i] = uint64(i + 1) + cpuTrace[i] = uint64(1) + } + + ref := info.ContainerReference{ + Name: "container", + } + + trace := buildTrace(cpuTrace, memTrace, 1*time.Second) + + for _, stats := range trace { + driver.AddStats(ref, stats) + } + + recentStats, err := driver.RecentStats(ref.Name, 10) + if err != nil { + t.Fatal(err) + } + if len(recentStats) == 0 { + t.Fatal("should at least store one stats") + } + + if len(recentStats) > 10 { + t.Fatalf("returned %v stats, not 10.", len(recentStats)) + } + + actualRecentStats := trace[len(trace)-len(recentStats):] + + // The returned stats should be sorted in time increasing order + for i, s := range actualRecentStats { + r := recentStats[i] + if !statsEq(s, r) { + t.Errorf("unexpected stats %+v with memory usage %v", r, r.Memory.Usage) + } + } +} + +func StorageDriverTestRetrieveAllRecentStats(driver storage.StorageDriver, t *testing.T) { + defer driver.Close() + N := 100 + memTrace := make([]uint64, N) + cpuTrace := make([]uint64, N) + for i := 0; i < N; i++ { + memTrace[i] = uint64(i + 1) + cpuTrace[i] = uint64(1) + } + + ref := info.ContainerReference{ + Name: "container", + } + + trace := buildTrace(cpuTrace, memTrace, 1*time.Second) + + for _, stats := range trace { + driver.AddStats(ref, stats) + } + + recentStats, err := driver.RecentStats(ref.Name, -1) + if err != nil { + t.Fatal(err) + } + if len(recentStats) == 0 { + t.Fatal("should at least store one stats") + } + if len(recentStats) > N { + t.Fatalf("returned %v stats, not 100.", len(recentStats)) + } + + actualRecentStats := trace[len(trace)-len(recentStats):] + + // The returned stats should be sorted in time increasing order + for i, s := range actualRecentStats { + r := recentStats[i] + if !statsEq(s, r) { + t.Errorf("unexpected stats %+v with memory usage %v", r, r.Memory.Usage) + } + } +} + +func StorageDriverTestNoRecentStats(driver storage.StorageDriver, t *testing.T) { + defer driver.Close() + nonExistContainer := "somerandomecontainer" + stats, _ := driver.RecentStats(nonExistContainer, -1) + if len(stats) > 0 { + t.Errorf("RecentStats() returns %v stats on non exist container", len(stats)) + } +} + +func StorageDriverTestNoSamples(driver storage.StorageDriver, t *testing.T) { + defer driver.Close() + nonExistContainer := "somerandomecontainer" + samples, _ := driver.Samples(nonExistContainer, -1) + if len(samples) > 0 { + t.Errorf("Samples() returns %v samples on non exist container", len(samples)) + } +} + +func StorageDriverTestPercentilesWithoutStats(driver storage.StorageDriver, t *testing.T) { + defer driver.Close() + nonExistContainer := "somerandomecontainer" + percentiles, _ := driver.Percentiles(nonExistContainer, []int{50, 80}, []int{50, 80}) + if percentiles == nil { + return + } + if percentiles.MaxMemoryUsage != 0 { + t.Errorf("Percentiles() reports max memory usage > 0 when there's no stats.") + } + for _, p := range percentiles.CpuUsagePercentiles { + if p.Value != 0 { + t.Errorf("Percentiles() reports cpu usage is %v when there's no stats.", p.Value) + } + } + for _, p := range percentiles.MemoryUsagePercentiles { + if p.Value != 0 { + t.Errorf("Percentiles() reports memory usage is %v when there's no stats.", p.Value) + } + } +} diff --git a/third_party/src/github.com/google/cadvisor/storagedriver.go b/third_party/src/github.com/google/cadvisor/storagedriver.go new file mode 100644 index 00000000000..3de3f85dd83 --- /dev/null +++ b/third_party/src/github.com/google/cadvisor/storagedriver.go @@ -0,0 +1,71 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "flag" + "fmt" + "os" + "time" + + "github.com/google/cadvisor/storage" + "github.com/google/cadvisor/storage/influxdb" + "github.com/google/cadvisor/storage/memory" +) + +var argSampleSize = flag.Int("samples", 1024, "number of samples we want to keep") +var argHistoryDuration = flag.Int("history_duration", 60, "number of seconds of container history to keep") +var argDbUsername = flag.String("storage_driver_user", "root", "database username") +var argDbPassword = flag.String("storage_driver_password", "root", "database password") +var argDbHost = flag.String("storage_driver_host", "localhost:8086", "database host:port") +var argDbName = flag.String("storage_driver_name", "cadvisor", "database name") +var argDbIsSecure = flag.Bool("storage_driver_secure", false, "use secure connection with database") + +func NewStorageDriver(driverName string) (storage.StorageDriver, error) { + var storageDriver storage.StorageDriver + var err error + switch driverName { + case "": + // empty string by default is the in memory store + fallthrough + case "memory": + storageDriver = memory.New(*argSampleSize, *argHistoryDuration) + return storageDriver, nil + case "influxdb": + var hostname string + hostname, err = os.Hostname() + if err != nil { + return nil, err + } + + storageDriver, err = influxdb.New( + hostname, + "cadvisorTable", + *argDbName, + *argDbUsername, + *argDbPassword, + *argDbHost, + *argDbIsSecure, + // TODO(monnand): One hour? Or user-defined? + 1*time.Hour, + ) + default: + err = fmt.Errorf("Unknown database driver: %v", *argDbDriver) + } + if err != nil { + return nil, err + } + return storageDriver, nil +}