diff --git a/api/handler.go b/api/handler.go index 409dc917..393705bd 100644 --- a/api/handler.go +++ b/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/cadvisor.go b/cadvisor.go index a1cf4949..ca35a678 100644 --- a/cadvisor.go +++ b/cadvisor.go @@ -81,7 +81,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/client/client.go b/client/client.go index d521b1e3..033cbc2a 100644 --- a/client/client.go +++ b/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/client/client_test.go b/client/client_test.go index 6fc8eaad..a5b7f691 100644 --- a/client/client_test.go +++ b/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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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_usage": [ - 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/info/container.go b/info/container.go index 6e987916..cd6a7175 100644 --- a/info/container.go +++ b/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 @@ -219,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"` diff --git a/info/test/datagen.go b/info/test/datagen.go index 36841774..a1c0a356 100644 --- a/info/test/datagen.go +++ b/info/test/datagen.go @@ -15,6 +15,7 @@ package test import ( + "fmt" "math" "math/rand" "time" @@ -46,6 +47,7 @@ func GenerateRandomStats(numStats, numCores int, duration time.Duration) []*info 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 } @@ -66,3 +68,60 @@ func GenerateRandomContainerSpec(numCores int) *info.ContainerSpec { 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/manager/manager.go b/manager/manager.go index 1b050355..696e0bad 100644 --- a/manager/manager.go +++ b/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/manager/manager_test.go b/manager/manager_test.go new file mode 100644 index 00000000..99862c5d --- /dev/null +++ b/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/pages/containers.go b/pages/containers.go index 58605a8b..b3302b17 100644 --- a/pages/containers.go +++ b/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) }