From 611b5c8bf56fc323b64ad44a45de58f14133a5c3 Mon Sep 17 00:00:00 2001 From: Piotr Szczesniak Date: Thu, 30 Jul 2015 15:32:46 +0200 Subject: [PATCH] Updated cadvisor dependency Also added a new dep: gcloud-golang/compute/metadata. --- Godeps/Godeps.json | 84 +++--- .../compute/metadata/metadata.go | 279 ++++++++++++++++++ .../google/cadvisor/api/versions.go | 45 ++- .../cadvisor/collector/collector_manager.go | 54 +++- .../collector/collector_manager_test.go | 12 +- .../google/cadvisor/collector/config.go | 50 ++++ .../collector/config/sample_config.json | 34 +++ .../google/cadvisor/collector/fakes.go | 12 +- .../cadvisor/collector/generic_collector.go | 165 +++++++++++ .../collector/generic_collector_test.go | 167 +++++++++++ .../google/cadvisor/collector/types.go | 12 +- .../google/cadvisor/container/container.go | 3 + .../cadvisor/container/docker/handler.go | 8 +- .../{compatability.go => compatibility.go} | 0 .../container/libcontainer/helpers.go | 4 +- .../google/cadvisor/container/mock.go | 5 + .../google/cadvisor/container/raw/handler.go | 37 ++- .../google/cadvisor/info/v1/container.go | 9 + .../google/cadvisor/info/v1/machine.go | 22 ++ .../google/cadvisor/info/{v2 => v1}/metric.go | 41 ++- .../google/cadvisor/info/v2/container.go | 10 + .../google/cadvisor/info/v2/machine.go | 8 + .../google/cadvisor/manager/container.go | 185 +++++++++--- .../google/cadvisor/manager/container_test.go | 2 +- .../google/cadvisor/manager/machine.go | 202 +------------ .../google/cadvisor/manager/manager.go | 97 ++++-- .../google/cadvisor/manager/manager_test.go | 4 +- .../google/cadvisor/pages/containers.go | 11 +- .../google/cadvisor/pages/docker.go | 2 +- .../github.com/google/cadvisor/pages/pages.go | 10 + .../google/cadvisor/storage/redis/redis.go | 8 +- .../storage/statsd/{ => client}/client.go | 43 ++- .../google/cadvisor/storage/statsd/statsd.go | 127 ++++++++ .../google/cadvisor/summary/percentiles.go | 45 +-- .../cadvisor/summary/percentiles_test.go | 110 ++++--- .../cadvisor/utils/cloudinfo/cloudinfo.go | 87 ++++++ .../google/cadvisor/utils/cloudinfo/gce.go | 36 +++ .../google/cadvisor/utils/fs/mockfs/mockfs.go | 2 +- .../google/cadvisor/utils/machine/machine.go | 243 +++++++++++++++ .../machine}/testdata/cpuinfo | 0 .../machine}/topology_test.go | 8 +- .../google/cadvisor/version/version.go | 2 +- 42 files changed, 1805 insertions(+), 480 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/GoogleCloudPlatform/gcloud-golang/compute/metadata/metadata.go create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/collector/config.go create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/collector/config/sample_config.json create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector.go create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector_test.go rename Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/{compatability.go => compatibility.go} (100%) rename Godeps/_workspace/src/github.com/google/cadvisor/info/{v2 => v1}/metric.go (63%) rename Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/{ => client}/client.go (61%) create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/statsd.go create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/cloudinfo.go create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/gce.go create mode 100644 Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/machine.go rename Godeps/_workspace/src/github.com/google/cadvisor/{manager => utils/machine}/testdata/cpuinfo (100%) rename Godeps/_workspace/src/github.com/google/cadvisor/{manager => utils/machine}/topology_test.go (94%) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index a68293e5fed..efcf2b5971d 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -34,6 +34,10 @@ "Comment": "release-96", "Rev": "98c78185197025f935947caac56a7b6d022f89d2" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/gcloud-golang/compute/metadata", + "Rev": "e34a32f9b0ecbc0784865fb2d47f3818c09521d4" + }, { "ImportPath": "github.com/Sirupsen/logrus", "Comment": "v0.6.2-10-g51fe59a", @@ -246,93 +250,93 @@ }, { "ImportPath": "github.com/google/cadvisor/api", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/cache/memory", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/collector", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/container", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/events", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/fs", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/healthz", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/http", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/info/v1", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/info/v2", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/manager", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/metrics", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/pages", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/storage", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/summary", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/utils", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/validate", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/cadvisor/version", - "Comment": "0.15.1", - "Rev": "ec588def40e1bb59f28f5a293b279f6762d13d44" + "Comment": "0.16.0-51-g78419de", + "Rev": "78419de3ea9c2d23cb04ec9d63f8899de34ebd43" }, { "ImportPath": "github.com/google/go-github/github", @@ -425,10 +429,10 @@ "ImportPath": "github.com/mitchellh/mapstructure", "Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf" }, - { - "ImportPath": "github.com/mxk/go-flowrate/flowrate", - "Rev": "cca7078d478f8520f85629ad7c68962d31ed7682" - }, + { + "ImportPath": "github.com/mxk/go-flowrate/flowrate", + "Rev": "cca7078d478f8520f85629ad7c68962d31ed7682" + }, { "ImportPath": "github.com/onsi/ginkgo", "Comment": "v1.2.0-6-gd981d36", diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/gcloud-golang/compute/metadata/metadata.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/gcloud-golang/compute/metadata/metadata.go new file mode 100644 index 00000000000..b007cde6366 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/gcloud-golang/compute/metadata/metadata.go @@ -0,0 +1,279 @@ +// 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 metadata provides access to Google Compute Engine (GCE) +// metadata and API service accounts. +// +// This package is a wrapper around the GCE metadata service, +// as documented at https://developers.google.com/compute/docs/metadata. +package metadata + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "strings" + "sync" + "time" + + "google.golang.org/cloud/internal" +) + +type cachedValue struct { + k string + trim bool + mu sync.Mutex + v string +} + +var ( + projID = &cachedValue{k: "project/project-id", trim: true} + projNum = &cachedValue{k: "project/numeric-project-id", trim: true} + instID = &cachedValue{k: "instance/id", trim: true} +) + +var metaClient = &http.Client{ + Transport: &internal.Transport{ + Base: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 750 * time.Millisecond, + KeepAlive: 30 * time.Second, + }).Dial, + ResponseHeaderTimeout: 750 * time.Millisecond, + }, + }, +} + +// NotDefinedError is returned when requested metadata is not defined. +// +// The underlying string is the suffix after "/computeMetadata/v1/". +// +// This error is not returned if the value is defined to be the empty +// string. +type NotDefinedError string + +func (suffix NotDefinedError) Error() string { + return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix)) +} + +// Get returns a value from the metadata service. +// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/". +// +// If the GCE_METADATA_HOST environment variable is not defined, a default of +// 169.254.169.254 will be used instead. +// +// If the requested metadata is not defined, the returned error will +// be of type NotDefinedError. +func Get(suffix string) (string, error) { + // Using a fixed IP makes it very difficult to spoof the metadata service in + // a container, which is an important use-case for local testing of cloud + // deployments. To enable spoofing of the metadata service, the environment + // variable GCE_METADATA_HOST is first inspected to decide where metadata + // requests shall go. + host := os.Getenv("GCE_METADATA_HOST") + if host == "" { + // Using 169.254.169.254 instead of "metadata" here because Go + // binaries built with the "netgo" tag and without cgo won't + // know the search suffix for "metadata" is + // ".google.internal", and this IP address is documented as + // being stable anyway. + host = "169.254.169.254" + } + url := "http://" + host + "/computeMetadata/v1/" + suffix + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Metadata-Flavor", "Google") + res, err := metaClient.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + if res.StatusCode == http.StatusNotFound { + return "", NotDefinedError(suffix) + } + if res.StatusCode != 200 { + return "", fmt.Errorf("status code %d trying to fetch %s", res.StatusCode, url) + } + all, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + return string(all), nil +} + +func getTrimmed(suffix string) (s string, err error) { + s, err = Get(suffix) + s = strings.TrimSpace(s) + return +} + +func (c *cachedValue) get() (v string, err error) { + defer c.mu.Unlock() + c.mu.Lock() + if c.v != "" { + return c.v, nil + } + if c.trim { + v, err = getTrimmed(c.k) + } else { + v, err = Get(c.k) + } + if err == nil { + c.v = v + } + return +} + +var onGCE struct { + sync.Mutex + set bool + v bool +} + +// OnGCE reports whether this process is running on Google Compute Engine. +func OnGCE() bool { + defer onGCE.Unlock() + onGCE.Lock() + if onGCE.set { + return onGCE.v + } + onGCE.set = true + + // We use the DNS name of the metadata service here instead of the IP address + // because we expect that to fail faster in the not-on-GCE case. + res, err := metaClient.Get("http://metadata.google.internal") + if err != nil { + return false + } + onGCE.v = res.Header.Get("Metadata-Flavor") == "Google" + return onGCE.v +} + +// ProjectID returns the current instance's project ID string. +func ProjectID() (string, error) { return projID.get() } + +// NumericProjectID returns the current instance's numeric project ID. +func NumericProjectID() (string, error) { return projNum.get() } + +// InternalIP returns the instance's primary internal IP address. +func InternalIP() (string, error) { + return getTrimmed("instance/network-interfaces/0/ip") +} + +// ExternalIP returns the instance's primary external (public) IP address. +func ExternalIP() (string, error) { + return getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip") +} + +// Hostname returns the instance's hostname. This will be of the form +// ".c..internal". +func Hostname() (string, error) { + return getTrimmed("instance/hostname") +} + +// InstanceTags returns the list of user-defined instance tags, +// assigned when initially creating a GCE instance. +func InstanceTags() ([]string, error) { + var s []string + j, err := Get("instance/tags") + if err != nil { + return nil, err + } + if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil { + return nil, err + } + return s, nil +} + +// InstanceID returns the current VM's numeric instance ID. +func InstanceID() (string, error) { + return instID.get() +} + +// InstanceName returns the current VM's instance ID string. +func InstanceName() (string, error) { + host, err := Hostname() + if err != nil { + return "", err + } + return strings.Split(host, ".")[0], nil +} + +// Zone returns the current VM's zone, such as "us-central1-b". +func Zone() (string, error) { + zone, err := getTrimmed("instance/zone") + // zone is of the form "projects//zones/". + if err != nil { + return "", err + } + return zone[strings.LastIndex(zone, "/")+1:], nil +} + +// InstanceAttributes returns the list of user-defined attributes, +// assigned when initially creating a GCE VM instance. The value of an +// attribute can be obtained with InstanceAttributeValue. +func InstanceAttributes() ([]string, error) { return lines("instance/attributes/") } + +// ProjectAttributes returns the list of user-defined attributes +// applying to the project as a whole, not just this VM. The value of +// an attribute can be obtained with ProjectAttributeValue. +func ProjectAttributes() ([]string, error) { return lines("project/attributes/") } + +func lines(suffix string) ([]string, error) { + j, err := Get(suffix) + if err != nil { + return nil, err + } + s := strings.Split(strings.TrimSpace(j), "\n") + for i := range s { + s[i] = strings.TrimSpace(s[i]) + } + return s, nil +} + +// InstanceAttributeValue returns the value of the provided VM +// instance attribute. +// +// If the requested attribute is not defined, the returned error will +// be of type NotDefinedError. +// +// InstanceAttributeValue may return ("", nil) if the attribute was +// defined to be the empty string. +func InstanceAttributeValue(attr string) (string, error) { + return Get("instance/attributes/" + attr) +} + +// ProjectAttributeValue returns the value of the provided +// project attribute. +// +// If the requested attribute is not defined, the returned error will +// be of type NotDefinedError. +// +// ProjectAttributeValue may return ("", nil) if the attribute was +// defined to be the empty string. +func ProjectAttributeValue(attr string) (string, error) { + return Get("project/attributes/" + attr) +} + +// Scopes returns the service account scopes for the given account. +// The account may be empty or the string "default" to use the instance's +// main account. +func Scopes(serviceAccount string) ([]string, error) { + if serviceAccount == "" { + serviceAccount = "default" + } + return lines("instance/service-accounts/" + serviceAccount + "/scopes") +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/api/versions.go b/Godeps/_workspace/src/github.com/google/cadvisor/api/versions.go index bac878e4fd5..8546e358de4 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/api/versions.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/api/versions.go @@ -39,6 +39,7 @@ const ( attributesApi = "attributes" versionApi = "version" psApi = "ps" + customMetricsApi = "appmetrics" ) // Interface for a cAdvisor API version @@ -305,7 +306,7 @@ func (self *version2_0) Version() string { } func (self *version2_0) SupportedRequestTypes() []string { - return []string{versionApi, attributesApi, eventsApi, machineApi, summaryApi, statsApi, specApi, storageApi, psApi} + return []string{versionApi, attributesApi, eventsApi, machineApi, summaryApi, statsApi, specApi, storageApi, psApi, customMetricsApi} } func (self *version2_0) HandleRequest(requestType string, request []string, m manager.Manager, w http.ResponseWriter, r *http.Request) error { @@ -364,6 +365,32 @@ func (self *version2_0) HandleRequest(requestType string, request []string, m ma contStats[name] = convertStats(cont) } return writeResult(contStats, w) + case customMetricsApi: + containerName := getContainerName(request) + glog.V(4).Infof("Api - Custom Metrics: Looking for metrics for container %q, options %+v", containerName, opt) + conts, err := m.GetRequestedContainersInfo(containerName, opt) + if err != nil { + return err + } + specs, err := m.GetContainerSpec(containerName, opt) + if err != nil { + return err + } + contMetrics := make(map[string]map[string][]info.MetricVal, 0) + for _, cont := range conts { + metrics := map[string][]info.MetricVal{} + contStats := convertStats(cont) + spec := specs[cont.Name] + for _, contStat := range contStats { + for _, ms := range spec.CustomMetrics { + if contStat.HasCustomMetrics && !contStat.CustomMetrics[ms.Name].Timestamp.IsZero() { + metrics[ms.Name] = append(metrics[ms.Name], contStat.CustomMetrics[ms.Name]) + } + } + } + contMetrics[containerName] = metrics + } + return writeResult(contMetrics, w) case specApi: containerName := getContainerName(request) glog.V(4).Infof("Api - Spec for container %q, options %+v", containerName, opt) @@ -412,12 +439,13 @@ func convertStats(cont *info.ContainerInfo) []v2.ContainerStats { stats := []v2.ContainerStats{} for _, val := range cont.Stats { stat := v2.ContainerStats{ - Timestamp: val.Timestamp, - HasCpu: cont.Spec.HasCpu, - HasMemory: cont.Spec.HasMemory, - HasNetwork: cont.Spec.HasNetwork, - HasFilesystem: cont.Spec.HasFilesystem, - HasDiskIo: cont.Spec.HasDiskIo, + Timestamp: val.Timestamp, + HasCpu: cont.Spec.HasCpu, + HasMemory: cont.Spec.HasMemory, + HasNetwork: cont.Spec.HasNetwork, + HasFilesystem: cont.Spec.HasFilesystem, + HasDiskIo: cont.Spec.HasDiskIo, + HasCustomMetrics: cont.Spec.HasCustomMetrics, } if stat.HasCpu { stat.Cpu = val.Cpu @@ -434,6 +462,9 @@ func convertStats(cont *info.ContainerInfo) []v2.ContainerStats { if stat.HasDiskIo { stat.DiskIo = val.DiskIo } + if stat.HasCustomMetrics { + stat.CustomMetrics = val.CustomMetrics + } // TODO(rjnagal): Handle load stats. stats = append(stats, stat) } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager.go b/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager.go index a3df12ccbd9..bac016b33b2 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager.go @@ -19,14 +19,15 @@ import ( "strings" "time" - "github.com/google/cadvisor/info/v2" + "github.com/google/cadvisor/info/v1" ) -type collectorManager struct { - collectors []*collectorData -} +const metricLabelPrefix = "io.cadvisor.metric." -var _ CollectorManager = &collectorManager{} +type GenericCollectorManager struct { + Collectors []*collectorData + NextCollectionTime time.Time +} type collectorData struct { collector Collector @@ -35,33 +36,54 @@ type collectorData struct { // Returns a new CollectorManager that is thread-compatible. func NewCollectorManager() (CollectorManager, error) { - return &collectorManager{ - collectors: []*collectorData{}, + return &GenericCollectorManager{ + Collectors: []*collectorData{}, + NextCollectionTime: time.Now(), }, nil } -func (cm *collectorManager) RegisterCollector(collector Collector) error { - cm.collectors = append(cm.collectors, &collectorData{ +func GetCollectorConfigs(labels map[string]string) map[string]string { + configs := map[string]string{} + for k, v := range labels { + if strings.HasPrefix(k, metricLabelPrefix) { + name := strings.TrimPrefix(k, metricLabelPrefix) + configs[name] = v + } + } + return configs +} + +func (cm *GenericCollectorManager) RegisterCollector(collector Collector) error { + cm.Collectors = append(cm.Collectors, &collectorData{ collector: collector, nextCollectionTime: time.Now(), }) return nil } -func (cm *collectorManager) Collect() (time.Time, []v2.Metric, error) { +func (cm *GenericCollectorManager) GetSpec() ([]v1.MetricSpec, error) { + metricSpec := []v1.MetricSpec{} + for _, c := range cm.Collectors { + specs := c.collector.GetSpec() + metricSpec = append(metricSpec, specs...) + } + + return metricSpec, nil +} + +func (cm *GenericCollectorManager) Collect() (time.Time, map[string]v1.MetricVal, error) { var errors []error // Collect from all collectors that are ready. var next time.Time - var metrics []v2.Metric - for _, c := range cm.collectors { + metrics := map[string]v1.MetricVal{} + for _, c := range cm.Collectors { if c.nextCollectionTime.Before(time.Now()) { - nextCollection, newMetrics, err := c.collector.Collect() + var err error + c.nextCollectionTime, metrics, err = c.collector.Collect(metrics) if err != nil { errors = append(errors, err) } - metrics = append(metrics, newMetrics...) - c.nextCollectionTime = nextCollection } // Keep track of the next collector that will be ready. @@ -69,7 +91,7 @@ func (cm *collectorManager) Collect() (time.Time, []v2.Metric, error) { next = c.nextCollectionTime } } - + cm.NextCollectionTime = next return next, metrics, compileErrors(errors) } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager_test.go b/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager_test.go index 49877032e4f..2ffb0087e47 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager_test.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/collector_manager_test.go @@ -18,7 +18,7 @@ import ( "testing" "time" - "github.com/google/cadvisor/info/v2" + "github.com/google/cadvisor/info/v1" "github.com/stretchr/testify/assert" ) @@ -28,17 +28,21 @@ type fakeCollector struct { collectedFrom int } -func (fc *fakeCollector) Collect() (time.Time, []v2.Metric, error) { +func (fc *fakeCollector) Collect(metric map[string]v1.MetricVal) (time.Time, map[string]v1.MetricVal, error) { fc.collectedFrom++ - return fc.nextCollectionTime, []v2.Metric{}, fc.err + return fc.nextCollectionTime, metric, fc.err } func (fc *fakeCollector) Name() string { return "fake-collector" } +func (fc *fakeCollector) GetSpec() []v1.MetricSpec { + return []v1.MetricSpec{} +} + func TestCollect(t *testing.T) { - cm := &collectorManager{} + cm := &GenericCollectorManager{} firstTime := time.Now().Add(-time.Hour) secondTime := time.Now().Add(time.Hour) diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/config.go b/Godeps/_workspace/src/github.com/google/cadvisor/collector/config.go new file mode 100644 index 00000000000..fc1207031ef --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/config.go @@ -0,0 +1,50 @@ +// Copyright 2015 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 collector + +import ( + "github.com/google/cadvisor/info/v1" + "time" +) + +type Config struct { + //the endpoint to hit to scrape metrics + Endpoint string `json:"endpoint"` + + //holds information about different metrics that can be collected + MetricsConfig []MetricConfig `json:"metrics_config"` +} + +// metricConfig holds information extracted from the config file about a metric +type MetricConfig struct { + //the name of the metric + Name string `json:"name"` + + //enum type for the metric type + MetricType v1.MetricType `json:"metric_type"` + + // metric units to display on UI and in storage (eg: MB, cores) + // this is only used for display. + Units string `json:"units"` + + //data type of the metric (eg: int, float) + DataType v1.DataType `json:"data_type"` + + //the frequency at which the metric should be collected + PollingFrequency time.Duration `json:"polling_frequency"` + + //the regular expression that can be used to extract the metric + Regex string `json:"regex"` +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/config/sample_config.json b/Godeps/_workspace/src/github.com/google/cadvisor/collector/config/sample_config.json new file mode 100644 index 00000000000..d1f9000cab4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/config/sample_config.json @@ -0,0 +1,34 @@ +{ + "endpoint" : "http://localhost:8000/nginx_status", + "metrics_config" : [ + { "name" : "activeConnections", + "metric_type" : "gauge", + "units" : "number of active connections", + "data_type" : "int", + "polling_frequency" : 10, + "regex" : "Active connections: ([0-9]+)" + }, + { "name" : "reading", + "metric_type" : "gauge", + "units" : "number of reading connections", + "data_type" : "int", + "polling_frequency" : 10, + "regex" : "Reading: ([0-9]+) .*" + }, + { "name" : "writing", + "metric_type" : "gauge", + "data_type" : "int", + "units" : "number of writing connections", + "polling_frequency" : 10, + "regex" : ".*Writing: ([0-9]+).*" + }, + { "name" : "waiting", + "metric_type" : "gauge", + "units" : "number of waiting connections", + "data_type" : "int", + "polling_frequency" : 10, + "regex" : ".*Waiting: ([0-9]+)" + } + ] + +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/fakes.go b/Godeps/_workspace/src/github.com/google/cadvisor/collector/fakes.go index 388f3fc0bfe..6b11acbb3ea 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/collector/fakes.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/fakes.go @@ -17,7 +17,7 @@ package collector import ( "time" - "github.com/google/cadvisor/info/v2" + "github.com/google/cadvisor/info/v1" ) type FakeCollectorManager struct { @@ -27,7 +27,11 @@ func (fkm *FakeCollectorManager) RegisterCollector(collector Collector) error { return nil } -func (fkm *FakeCollectorManager) Collect() (time.Time, []v2.Metric, error) { - var zero time.Time - return zero, []v2.Metric{}, nil +func (fkm *FakeCollectorManager) GetSpec() ([]v1.MetricSpec, error) { + return []v1.MetricSpec{}, nil +} + +func (fkm *FakeCollectorManager) Collect(metric map[string]v1.MetricVal) (time.Time, map[string]v1.MetricVal, error) { + var zero time.Time + return zero, metric, nil } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector.go b/Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector.go new file mode 100644 index 00000000000..6f724cc0230 --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector.go @@ -0,0 +1,165 @@ +// Copyright 2015 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 collector + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/google/cadvisor/info/v1" +) + +type GenericCollector struct { + //name of the collector + name string + + //holds information extracted from the config file for a collector + configFile Config + + //holds information necessary to extract metrics + info *collectorInfo +} + +type collectorInfo struct { + //minimum polling frequency among all metrics + minPollingFrequency time.Duration + + //regular expresssions for all metrics + regexps []*regexp.Regexp +} + +//Returns a new collector using the information extracted from the configfile +func NewCollector(collectorName string, configFile []byte) (*GenericCollector, error) { + var configInJSON Config + err := json.Unmarshal(configFile, &configInJSON) + if err != nil { + return nil, err + } + + //TODO : Add checks for validity of config file (eg : Accurate JSON fields) + + if len(configInJSON.MetricsConfig) == 0 { + return nil, fmt.Errorf("No metrics provided in config") + } + + minPollFrequency := time.Duration(0) + regexprs := make([]*regexp.Regexp, len(configInJSON.MetricsConfig)) + + for ind, metricConfig := range configInJSON.MetricsConfig { + // Find the minimum specified polling frequency in metric config. + if metricConfig.PollingFrequency != 0 { + if minPollFrequency == 0 || metricConfig.PollingFrequency < minPollFrequency { + minPollFrequency = metricConfig.PollingFrequency + } + } + + regexprs[ind], err = regexp.Compile(metricConfig.Regex) + if err != nil { + return nil, fmt.Errorf("Invalid regexp %v for metric %v", metricConfig.Regex, metricConfig.Name) + } + } + + // Minimum supported polling frequency is 1s. + minSupportedFrequency := 1 * time.Second + if minPollFrequency < minSupportedFrequency { + minPollFrequency = minSupportedFrequency + } + + return &GenericCollector{ + name: collectorName, + configFile: configInJSON, + info: &collectorInfo{ + minPollingFrequency: minPollFrequency, + regexps: regexprs}, + }, nil +} + +//Returns name of the collector +func (collector *GenericCollector) Name() string { + return collector.name +} + +func (collector *GenericCollector) configToSpec(config MetricConfig) v1.MetricSpec { + return v1.MetricSpec{ + Name: config.Name, + Type: config.MetricType, + Format: config.DataType, + Units: config.Units, + } +} + +func (collector *GenericCollector) GetSpec() []v1.MetricSpec { + specs := []v1.MetricSpec{} + for _, metricConfig := range collector.configFile.MetricsConfig { + spec := collector.configToSpec(metricConfig) + specs = append(specs, spec) + } + return specs +} + +//Returns collected metrics and the next collection time of the collector +func (collector *GenericCollector) Collect(metrics map[string]v1.MetricVal) (time.Time, map[string]v1.MetricVal, error) { + currentTime := time.Now() + nextCollectionTime := currentTime.Add(time.Duration(collector.info.minPollingFrequency)) + + uri := collector.configFile.Endpoint + response, err := http.Get(uri) + if err != nil { + return nextCollectionTime, nil, err + } + + defer response.Body.Close() + + pageContent, err := ioutil.ReadAll(response.Body) + if err != nil { + return nextCollectionTime, nil, err + } + + var errorSlice []error + for ind, metricConfig := range collector.configFile.MetricsConfig { + matchString := collector.info.regexps[ind].FindStringSubmatch(string(pageContent)) + if matchString != nil { + if metricConfig.DataType == v1.FloatType { + regVal, err := strconv.ParseFloat(strings.TrimSpace(matchString[1]), 64) + if err != nil { + errorSlice = append(errorSlice, err) + } + metrics[metricConfig.Name] = v1.MetricVal{ + FloatValue: regVal, Timestamp: currentTime, + } + } else if metricConfig.DataType == v1.IntType { + regVal, err := strconv.ParseInt(strings.TrimSpace(matchString[1]), 10, 64) + if err != nil { + errorSlice = append(errorSlice, err) + } + metrics[metricConfig.Name] = v1.MetricVal{ + IntValue: regVal, Timestamp: currentTime, + } + + } else { + errorSlice = append(errorSlice, fmt.Errorf("Unexpected value of 'data_type' for metric '%v' in config ", metricConfig.Name)) + } + } else { + errorSlice = append(errorSlice, fmt.Errorf("No match found for regexp: %v for metric '%v' in config", metricConfig.Regex, metricConfig.Name)) + } + } + return nextCollectionTime, metrics, compileErrors(errorSlice) +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector_test.go b/Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector_test.go new file mode 100644 index 00000000000..a1f850de229 --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/generic_collector_test.go @@ -0,0 +1,167 @@ +// Copyright 2015 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 collector + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/google/cadvisor/info/v1" + "github.com/stretchr/testify/assert" +) + +func TestEmptyConfig(t *testing.T) { + assert := assert.New(t) + + emptyConfig := ` + { + "endpoint" : "http://localhost:8000/nginx_status", + "metrics_config" : [ + ] + } + ` + + //Create a temporary config file 'temp.json' with invalid json format + assert.NoError(ioutil.WriteFile("temp.json", []byte(emptyConfig), 0777)) + + configFile, err := ioutil.ReadFile("temp.json") + assert.NoError(err) + + _, err = NewCollector("tempCollector", configFile) + assert.Error(err) + + assert.NoError(os.Remove("temp.json")) +} + +func TestConfigWithErrors(t *testing.T) { + assert := assert.New(t) + + //Syntax error: Missed '"' after activeConnections + invalid := ` + { + "endpoint" : "http://localhost:8000/nginx_status", + "metrics_config" : [ + { + "name" : "activeConnections, + "metric_type" : "gauge", + "data_type" : "int", + "polling_frequency" : 10, + "regex" : "Active connections: ([0-9]+)" + } + ] + } + ` + + //Create a temporary config file 'temp.json' with invalid json format + assert.NoError(ioutil.WriteFile("temp.json", []byte(invalid), 0777)) + configFile, err := ioutil.ReadFile("temp.json") + assert.NoError(err) + + _, err = NewCollector("tempCollector", configFile) + assert.Error(err) + + assert.NoError(os.Remove("temp.json")) +} + +func TestConfigWithRegexErrors(t *testing.T) { + assert := assert.New(t) + + //Error: Missed operand for '+' in activeConnections regex + invalid := ` + { + "endpoint" : "host:port/nginx_status", + "metrics_config" : [ + { + "name" : "activeConnections", + "metric_type" : "gauge", + "data_type" : "int", + "polling_frequency" : 10, + "regex" : "Active connections: (+)" + }, + { + "name" : "reading", + "metric_type" : "gauge", + "data_type" : "int", + "polling_frequency" : 10, + "regex" : "Reading: ([0-9]+) .*" + } + ] + } + ` + + //Create a temporary config file 'temp.json' + assert.NoError(ioutil.WriteFile("temp.json", []byte(invalid), 0777)) + + configFile, err := ioutil.ReadFile("temp.json") + assert.NoError(err) + + _, err = NewCollector("tempCollector", configFile) + assert.Error(err) + + assert.NoError(os.Remove("temp.json")) +} + +func TestConfig(t *testing.T) { + assert := assert.New(t) + + //Create an nginx collector using the config file 'sample_config.json' + configFile, err := ioutil.ReadFile("config/sample_config.json") + assert.NoError(err) + + collector, err := NewCollector("nginx", configFile) + assert.NoError(err) + assert.Equal(collector.name, "nginx") + assert.Equal(collector.configFile.Endpoint, "http://localhost:8000/nginx_status") + assert.Equal(collector.configFile.MetricsConfig[0].Name, "activeConnections") +} + +func TestMetricCollection(t *testing.T) { + assert := assert.New(t) + + //Collect nginx metrics from a fake nginx endpoint + configFile, err := ioutil.ReadFile("config/sample_config.json") + assert.NoError(err) + + fakeCollector, err := NewCollector("nginx", configFile) + assert.NoError(err) + + tempServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Active connections: 3\nserver accepts handled requests") + fmt.Fprintln(w, "5 5 32\nReading: 0 Writing: 1 Waiting: 2") + })) + defer tempServer.Close() + fakeCollector.configFile.Endpoint = tempServer.URL + + metrics := map[string]v1.MetricVal{} + _, metrics, errMetric := fakeCollector.Collect(metrics) + assert.NoError(errMetric) + metricNames := []string{"activeConnections", "reading", "writing", "waiting"} + // activeConnections = 3 + assert.Equal(metrics[metricNames[0]].IntValue, 3) + assert.Equal(metrics[metricNames[0]].FloatValue, 0) + // reading = 0 + assert.Equal(metrics[metricNames[1]].IntValue, 0) + assert.Equal(metrics[metricNames[1]].FloatValue, 0) + // writing = 1 + assert.Equal(metrics[metricNames[2]].IntValue, 1) + assert.Equal(metrics[metricNames[2]].FloatValue, 0) + // waiting = 2 + assert.Equal(metrics[metricNames[3]].IntValue, 2) + assert.Equal(metrics[metricNames[3]].FloatValue, 0) +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/collector/types.go b/Godeps/_workspace/src/github.com/google/cadvisor/collector/types.go index 8bd29d0604a..60851bee2b0 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/collector/types.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/collector/types.go @@ -15,7 +15,7 @@ package collector import ( - "github.com/google/cadvisor/info/v2" + "github.com/google/cadvisor/info/v1" "time" ) @@ -27,7 +27,10 @@ type Collector interface { // Returns the next time this collector should be collected from. // Next collection time is always returned, even when an error occurs. // A collection time of zero means no more collection. - Collect() (time.Time, []v2.Metric, error) + Collect(map[string]v1.MetricVal) (time.Time, map[string]v1.MetricVal, error) + + // Return spec for all metrics associated with this collector + GetSpec() []v1.MetricSpec // Name of this collector. Name() string @@ -42,5 +45,8 @@ type CollectorManager interface { // at which a collector will be ready to collect from. // Next collection time is always returned, even when an error occurs. // A collection time of zero means no more collection. - Collect() (time.Time, []v2.Metric, error) + Collect() (time.Time, map[string]v1.MetricVal, error) + + // Get metric spec from all registered collectors. + GetSpec() ([]v1.MetricSpec, error) } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/container/container.go b/Godeps/_workspace/src/github.com/google/cadvisor/container/container.go index b62e3e9661c..2ab5dfdfb39 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/container/container.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/container/container.go @@ -73,6 +73,9 @@ type ContainerHandler interface { // Returns absolute cgroup path for the requested resource. GetCgroupPath(resource string) (string, error) + // Returns container labels, if available. + GetContainerLabels() map[string]string + // Returns whether the container still exists. Exists() bool } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/container/docker/handler.go b/Godeps/_workspace/src/github.com/google/cadvisor/container/docker/handler.go index 6d8ca8c7f09..2344fc05b53 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/container/docker/handler.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/container/docker/handler.go @@ -167,7 +167,7 @@ func libcontainerConfigToContainerSpec(config *libcontainerConfigs.Config, mi *i } spec.Cpu.Mask = utils.FixCpuMask(config.Cgroups.CpusetCpus, mi.NumCores) - spec.HasNetwork = true + spec.HasNetwork = len(config.Networks) > 0 spec.HasDiskIo = true return spec @@ -276,7 +276,7 @@ func (self *dockerContainerHandler) GetStats() (*info.ContainerStats, error) { } func convertInterfaceStats(stats *info.InterfaceStats) { - net := stats + net := *stats // Ingress for host veth is from the container. // Hence tx_bytes stat on the host veth is actually number of bytes received by the container. @@ -332,6 +332,10 @@ func (self *dockerContainerHandler) ListThreads(listType container.ListType) ([] return nil, nil } +func (self *dockerContainerHandler) GetContainerLabels() map[string]string { + return self.labels +} + func (self *dockerContainerHandler) ListProcesses(listType container.ListType) ([]int, error) { return containerLibcontainer.GetProcesses(self.cgroupManager) } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/compatability.go b/Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/compatibility.go similarity index 100% rename from Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/compatability.go rename to Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/compatibility.go diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/helpers.go b/Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/helpers.go index bb6e9ee3091..0cd4c119b9d 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/helpers.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/container/libcontainer/helpers.go @@ -93,7 +93,7 @@ func GetStats(cgroupManager cgroups.Manager, networkInterfaces []string) (*info. } stats.Network.Interfaces[i] = interfaceStats } - // For backwards compatability. + // For backwards compatibility. if len(networkInterfaces) > 0 { stats.Network.InterfaceStats = stats.Network.Interfaces[0] } @@ -233,7 +233,7 @@ func toContainerStats3(libcontainerStats *libcontainer.Stats, ret *info.Containe } } - // Add to base struct for backwards compatability. + // Add to base struct for backwards compatibility. if len(ret.Network.Interfaces) > 0 { ret.Network.InterfaceStats = ret.Network.Interfaces[0] } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/container/mock.go b/Godeps/_workspace/src/github.com/google/cadvisor/container/mock.go index 1d5498854f2..7422b3ddbc1 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/container/mock.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/container/mock.go @@ -95,6 +95,11 @@ func (self *MockContainerHandler) GetCgroupPath(path string) (string, error) { return args.Get(0).(string), args.Error(1) } +func (self *MockContainerHandler) GetContainerLabels() map[string]string { + args := self.Called() + return args.Get(0).(map[string]string) +} + type FactoryForMockContainerHandler struct { Name string PrepareContainerHandlerFunc func(name string, handler *MockContainerHandler) diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/container/raw/handler.go b/Godeps/_workspace/src/github.com/google/cadvisor/container/raw/handler.go index 33f1de677e0..51405db74f7 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/container/raw/handler.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/container/raw/handler.go @@ -33,6 +33,7 @@ import ( "github.com/google/cadvisor/fs" info "github.com/google/cadvisor/info/v1" "github.com/google/cadvisor/utils" + "github.com/google/cadvisor/utils/machine" "golang.org/x/exp/inotify" ) @@ -210,13 +211,33 @@ func (self *rawContainerHandler) GetSpec() (info.ContainerSpec, error) { } } - // Memory. - memoryRoot, ok := self.cgroupPaths["memory"] - if ok { - if utils.FileExists(memoryRoot) { + // Memory + if self.name == "/" { + // Get memory and swap limits of the running machine + memLimit, err := machine.GetMachineMemoryCapacity() + if err != nil { + glog.Warningf("failed to obtain memory limit for machine container") + spec.HasMemory = false + } else { + spec.Memory.Limit = uint64(memLimit) + // Spec is marked to have memory only if the memory limit is set spec.HasMemory = true - spec.Memory.Limit = readInt64(memoryRoot, "memory.limit_in_bytes") - spec.Memory.SwapLimit = readInt64(memoryRoot, "memory.memsw.limit_in_bytes") + } + + swapLimit, err := machine.GetMachineSwapCapacity() + if err != nil { + glog.Warningf("failed to obtain swap limit for machine container") + } else { + spec.Memory.SwapLimit = uint64(swapLimit) + } + } else { + memoryRoot, ok := self.cgroupPaths["memory"] + if ok { + if utils.FileExists(memoryRoot) { + spec.HasMemory = true + spec.Memory.Limit = readInt64(memoryRoot, "memory.limit_in_bytes") + spec.Memory.SwapLimit = readInt64(memoryRoot, "memory.memsw.limit_in_bytes") + } } } @@ -335,6 +356,10 @@ func (self *rawContainerHandler) GetCgroupPath(resource string) (string, error) return path, nil } +func (self *rawContainerHandler) GetContainerLabels() map[string]string { + return map[string]string{} +} + // Lists all directories under "path" and outputs the results as children of "parent". func listDirectories(dirpath string, parent string, recursive bool, output map[string]struct{}) error { // Ignore if this hierarchy does not exist. diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/container.go b/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/container.go index e9afe26a713..1788d5818ff 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/container.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/container.go @@ -58,6 +58,9 @@ type ContainerSpec struct { // HasDiskIo when true, indicates that DiskIo stats will be available. HasDiskIo bool `json:"has_diskio"` + + HasCustomMetrics bool `json:"has_custom_metrics"` + CustomMetrics []MetricSpec `json:"custom_metrics,omitempty"` } // Container reference contains enough information to uniquely identify a container @@ -190,6 +193,9 @@ func (self *ContainerSpec) Eq(b *ContainerSpec) bool { if self.HasDiskIo != b.HasDiskIo { return false } + if self.HasCustomMetrics != b.HasCustomMetrics { + return false + } return true } @@ -419,6 +425,9 @@ type ContainerStats struct { // Task load stats TaskStats LoadStats `json:"task_stats,omitempty"` + + //Custom metrics from all collectors + CustomMetrics map[string]MetricVal `json:"custom_metrics,omitempty"` } func timeEq(t1, t2 time.Time, tolerance time.Duration) bool { diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/machine.go b/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/machine.go index ddc7452bfd8..dc07ffa67a2 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/machine.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/machine.go @@ -112,6 +112,22 @@ type NetInfo struct { Mtu int64 `json:"mtu"` } +type CloudProvider string + +const ( + GCE CloudProvider = "GCE" + AWS = "AWS" + Baremetal = "Baremetal" + UnkownProvider = "Unknown" +) + +type InstanceType string + +const ( + NoInstance InstanceType = "None" + UnknownInstance = "Unknown" +) + type MachineInfo struct { // The number of cores in this machine. NumCores int `json:"num_cores"` @@ -143,6 +159,12 @@ type MachineInfo struct { // Machine Topology // Describes cpu/memory layout and hierarchy. Topology []Node `json:"topology"` + + // Cloud provider the machine belongs to. + CloudProvider CloudProvider `json:"cloud_provider"` + + // Type of cloud instance (e.g. GCE standard) the machine is. + InstanceType InstanceType `json:"instance_type"` } type VersionInfo struct { diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/metric.go b/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/metric.go similarity index 63% rename from Godeps/_workspace/src/github.com/google/cadvisor/info/v2/metric.go rename to Godeps/_workspace/src/github.com/google/cadvisor/info/v1/metric.go index 1057980ce37..f1ffd7be472 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/metric.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/info/v1/metric.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package v2 +package v1 import ( "time" @@ -32,38 +32,35 @@ const ( MetricDelta = "delta" ) -// An exported metric. -type Metric struct { +// DataType for metric being exported. +type DataType string + +const ( + IntType DataType = "int" + FloatType = "float" +) + +// Spec for custom metric. +type MetricSpec struct { // The name of the metric. Name string `json:"name"` // Type of the metric. Type MetricType `json:"type"` - // Metadata associated with this metric. - Labels map[string]string + // Data Type for the stats. + Format DataType `json:"format"` - // Value of the metric. Only one of these values will be - // available according to the output type of the metric. - // If no values are available, there are no data points. - IntPoints []IntPoint `json:"int_points,omitempty"` - FloatPoints []FloatPoint `json:"float_points,omitempty"` + // Display Units for the stats. + Units string `json:"units"` } -// An integer metric data point. -type IntPoint struct { +// An exported metric. +type MetricVal struct { // Time at which the metric was queried Timestamp time.Time `json:"timestamp"` // The value of the metric at this point. - Value int64 `json:"value"` -} - -// A float metric data point. -type FloatPoint struct { - // Time at which the metric was queried - Timestamp time.Time `json:"timestamp"` - - // The value of the metric at this point. - Value float64 `json:"value"` + IntValue int64 `json:"int_value,omitempty"` + FloatValue float64 `json:"float_value,omitempty"` } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/container.go b/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/container.go index 7d9c388ce11..e4bda16e13a 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/container.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/container.go @@ -73,6 +73,9 @@ type ContainerSpec struct { HasMemory bool `json:"has_memory"` Memory MemorySpec `json:"memory,omitempty"` + HasCustomMetrics bool `json:"has_custom_metrics"` + CustomMetrics []v1.MetricSpec `json:"custom_metrics,omitempty"` + // Following resources have no associated spec, but are being isolated. HasNetwork bool `json:"has_network"` HasFilesystem bool `json:"has_filesystem"` @@ -100,6 +103,9 @@ type ContainerStats struct { // Task load statistics HasLoad bool `json:"has_load"` Load v1.LoadStats `json:"load_stats,omitempty"` + // Custom Metrics + HasCustomMetrics bool `json:"has_custom_metrics"` + CustomMetrics map[string]v1.MetricVal `json:"custom_metrics,omitempty"` } type Percentiles struct { @@ -110,8 +116,12 @@ type Percentiles struct { Mean uint64 `json:"mean"` // Max seen over the collected sample. Max uint64 `json:"max"` + // 50th percentile over the collected sample. + Fifty uint64 `json:"fifty"` // 90th percentile over the collected sample. Ninety uint64 `json:"ninety"` + // 95th percentile over the collected sample. + NinetyFive uint64 `json:"ninetyfive"` } type Usage struct { diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/machine.go b/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/machine.go index 219297933c7..4aef3d835fb 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/machine.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/info/v2/machine.go @@ -59,6 +59,12 @@ type Attributes struct { // Machine Topology // Describes cpu/memory layout and hierarchy. Topology []v1.Node `json:"topology"` + + // Cloud provider the machine belongs to + CloudProvider v1.CloudProvider `json:"cloud_provider"` + + // Type of cloud instance (e.g. GCE standard) the machine is. + InstanceType v1.InstanceType `json:"instance_type"` } func GetAttributes(mi *v1.MachineInfo, vi *v1.VersionInfo) Attributes { @@ -76,5 +82,7 @@ func GetAttributes(mi *v1.MachineInfo, vi *v1.VersionInfo) Attributes { DiskMap: mi.DiskMap, NetworkDevices: mi.NetworkDevices, Topology: mi.Topology, + CloudProvider: mi.CloudProvider, + InstanceType: mi.InstanceType, } } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/manager/container.go b/Godeps/_workspace/src/github.com/google/cadvisor/manager/container.go index 5498c674915..e6084801058 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/manager/container.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/manager/container.go @@ -17,8 +17,10 @@ package manager import ( "flag" "fmt" + "io/ioutil" "math" "os/exec" + "path" "regexp" "sort" "strconv" @@ -39,8 +41,6 @@ import ( // Housekeeping interval. var HousekeepingInterval = flag.Duration("housekeeping_interval", 1*time.Second, "Interval between container housekeepings") -var maxHousekeepingInterval = flag.Duration("max_housekeeping_interval", 60*time.Second, "Largest interval to allow between container housekeepings") -var allowDynamicHousekeeping = flag.Bool("allow_dynamic_housekeeping", true, "Whether to allow the housekeeping interval to be dynamic") var cgroupPathRegExp = regexp.MustCompile(".*:devices:(.*?),.*") @@ -54,16 +54,18 @@ type containerInfo struct { } type containerData struct { - handler container.ContainerHandler - info containerInfo - memoryCache *memory.InMemoryCache - lock sync.Mutex - loadReader cpuload.CpuLoadReader - summaryReader *summary.StatsSummary - loadAvg float64 // smoothed load average seen so far. - housekeepingInterval time.Duration - lastUpdatedTime time.Time - lastErrorTime time.Time + handler container.ContainerHandler + info containerInfo + memoryCache *memory.InMemoryCache + lock sync.Mutex + loadReader cpuload.CpuLoadReader + summaryReader *summary.StatsSummary + loadAvg float64 // smoothed load average seen so far. + housekeepingInterval time.Duration + maxHousekeepingInterval time.Duration + allowDynamicHousekeeping bool + lastUpdatedTime time.Time + lastErrorTime time.Time // Whether to log the usage of this container when it is updated. logUsage bool @@ -136,11 +138,32 @@ func (c *containerData) getCgroupPath(cgroups string) (string, error) { return string(matches[1]), nil } -func (c *containerData) GetProcessList(cadvisorContainer string, inHostNamespace bool) ([]v2.ProcessInfo, error) { - // report all processes for root. - isRoot := c.info.Name == "/" - // TODO(rjnagal): Take format as an option? - format := "user,pid,ppid,stime,pcpu,pmem,rss,vsz,stat,time,comm,cgroup" +// Returns contents of a file inside the container root. +// Takes in a path relative to container root. +func (c *containerData) ReadFile(filepath string, inHostNamespace bool) ([]byte, error) { + pids, err := c.getContainerPids(inHostNamespace) + if err != nil { + return nil, err + } + // TODO(rjnagal): Optimize by just reading container's cgroup.proc file when in host namespace. + rootfs := "/" + if !inHostNamespace { + rootfs = "/rootfs" + } + for _, pid := range pids { + filePath := path.Join(rootfs, "/proc", pid, "/root", filepath) + glog.V(3).Infof("Trying path %q", filePath) + data, err := ioutil.ReadFile(filePath) + if err == nil { + return data, err + } + } + // No process paths could be found. Declare config non-existent. + return nil, fmt.Errorf("file %q does not exist.", filepath) +} + +// Return output for ps command in host /proc with specified format +func (c *containerData) getPsOutput(inHostNamespace bool, format string) ([]byte, error) { args := []string{} command := "ps" if !inHostNamespace { @@ -148,11 +171,53 @@ func (c *containerData) GetProcessList(cadvisorContainer string, inHostNamespace args = append(args, "/rootfs", "ps") } args = append(args, "-e", "-o", format) - expectedFields := 12 out, err := exec.Command(command, args...).Output() if err != nil { return nil, fmt.Errorf("failed to execute %q command: %v", command, err) } + return out, err +} + +// Get pids of processes in this container. +// A slightly lighterweight call than GetProcessList if other details are not required. +func (c *containerData) getContainerPids(inHostNamespace bool) ([]string, error) { + format := "pid,cgroup" + out, err := c.getPsOutput(inHostNamespace, format) + if err != nil { + return nil, err + } + expectedFields := 2 + lines := strings.Split(string(out), "\n") + pids := []string{} + for _, line := range lines[1:] { + if len(line) == 0 { + continue + } + fields := strings.Fields(line) + if len(fields) < expectedFields { + return nil, fmt.Errorf("expected at least %d fields, found %d: output: %q", expectedFields, len(fields), line) + } + pid := fields[0] + cgroup, err := c.getCgroupPath(fields[1]) + if err != nil { + return nil, fmt.Errorf("could not parse cgroup path from %q: %v", fields[1], err) + } + if c.info.Name == cgroup { + pids = append(pids, pid) + } + } + return pids, nil +} + +func (c *containerData) GetProcessList(cadvisorContainer string, inHostNamespace bool) ([]v2.ProcessInfo, error) { + // report all processes for root. + isRoot := c.info.Name == "/" + format := "user,pid,ppid,stime,pcpu,pmem,rss,vsz,stat,time,comm,cgroup" + out, err := c.getPsOutput(inHostNamespace, format) + if err != nil { + return nil, err + } + expectedFields := 12 processes := []v2.ProcessInfo{} lines := strings.Split(string(out), "\n") for _, line := range lines[1:] { @@ -183,13 +248,17 @@ func (c *containerData) GetProcessList(cadvisorContainer string, inHostNamespace if err != nil { return nil, fmt.Errorf("invalid rss %q: %v", fields[6], err) } + // convert to bytes + rss *= 1024 vs, err := strconv.ParseUint(fields[7], 0, 64) if err != nil { return nil, fmt.Errorf("invalid virtual size %q: %v", fields[7], err) } + // convert to bytes + vs *= 1024 cgroup, err := c.getCgroupPath(fields[11]) if err != nil { - return nil, fmt.Errorf("could not parse cgroup path from %q: %v", fields[10], err) + return nil, fmt.Errorf("could not parse cgroup path from %q: %v", fields[11], err) } // Remove the ps command we just ran from cadvisor container. // Not necessary, but makes the cadvisor page look cleaner. @@ -221,7 +290,7 @@ func (c *containerData) GetProcessList(cadvisorContainer string, inHostNamespace return processes, nil } -func newContainerData(containerName string, memoryCache *memory.InMemoryCache, handler container.ContainerHandler, loadReader cpuload.CpuLoadReader, logUsage bool, collectorManager collector.CollectorManager) (*containerData, error) { +func newContainerData(containerName string, memoryCache *memory.InMemoryCache, handler container.ContainerHandler, loadReader cpuload.CpuLoadReader, logUsage bool, collectorManager collector.CollectorManager, maxHousekeepingInterval time.Duration, allowDynamicHousekeeping bool) (*containerData, error) { if memoryCache == nil { return nil, fmt.Errorf("nil memory storage") } @@ -234,14 +303,16 @@ func newContainerData(containerName string, memoryCache *memory.InMemoryCache, h } cont := &containerData{ - handler: handler, - memoryCache: memoryCache, - housekeepingInterval: *HousekeepingInterval, - loadReader: loadReader, - logUsage: logUsage, - loadAvg: -1.0, // negative value indicates uninitialized. - stop: make(chan bool, 1), - collectorManager: collectorManager, + handler: handler, + memoryCache: memoryCache, + housekeepingInterval: *HousekeepingInterval, + maxHousekeepingInterval: maxHousekeepingInterval, + allowDynamicHousekeeping: allowDynamicHousekeeping, + loadReader: loadReader, + logUsage: logUsage, + loadAvg: -1.0, // negative value indicates uninitialized. + stop: make(chan bool, 1), + collectorManager: collectorManager, } cont.info.ContainerReference = ref @@ -260,7 +331,7 @@ func newContainerData(containerName string, memoryCache *memory.InMemoryCache, h // Determine when the next housekeeping should occur. func (self *containerData) nextHousekeeping(lastHousekeeping time.Time) time.Time { - if *allowDynamicHousekeeping { + if self.allowDynamicHousekeeping { var empty time.Time stats, err := self.memoryCache.RecentStats(self.info.Name, empty, empty, 2) if err != nil { @@ -270,10 +341,10 @@ func (self *containerData) nextHousekeeping(lastHousekeeping time.Time) time.Tim } else if len(stats) == 2 { // TODO(vishnuk): Use no processes as a signal. // Raise the interval if usage hasn't changed in the last housekeeping. - if stats[0].StatsEq(stats[1]) && (self.housekeepingInterval < *maxHousekeepingInterval) { + if stats[0].StatsEq(stats[1]) && (self.housekeepingInterval < self.maxHousekeepingInterval) { self.housekeepingInterval *= 2 - if self.housekeepingInterval > *maxHousekeepingInterval { - self.housekeepingInterval = *maxHousekeepingInterval + if self.housekeepingInterval > self.maxHousekeepingInterval { + self.housekeepingInterval = self.maxHousekeepingInterval } } else if self.housekeepingInterval != *HousekeepingInterval { // Lower interval back to the baseline. @@ -340,19 +411,7 @@ func (c *containerData) housekeeping() { } } - // TODO(vmarmol): Export metrics. - // Run custom collectors. - nextCollectionTime, _, err := c.collectorManager.Collect() - if err != nil && c.allowErrorLogging() { - glog.Warningf("[%s] Collection failed: %v", c.info.Name, err) - } - - // Next housekeeping is the first of the stats or the custom collector's housekeeping. - nextHousekeeping := c.nextHousekeeping(lastHousekeeping) - next := nextHousekeeping - if !nextCollectionTime.IsZero() && nextCollectionTime.Before(nextHousekeeping) { - next = nextCollectionTime - } + next := c.nextHousekeeping(lastHousekeeping) // Schedule the next housekeeping. Sleep until that time. if time.Now().Before(next) { @@ -380,6 +439,12 @@ func (c *containerData) updateSpec() error { } return err } + + customMetrics, err := c.collectorManager.GetSpec() + if len(customMetrics) > 0 { + spec.HasCustomMetrics = true + spec.CustomMetrics = customMetrics + } c.lock.Lock() defer c.lock.Unlock() c.info.Spec = spec @@ -432,6 +497,20 @@ func (c *containerData) updateStats() error { glog.V(2).Infof("Failed to add summary stats for %q: %v", c.info.Name, err) } } + var customStatsErr error + cm := c.collectorManager.(*collector.GenericCollectorManager) + if len(cm.Collectors) > 0 { + if cm.NextCollectionTime.Before(time.Now()) { + customStats, err := c.updateCustomStats() + if customStats != nil { + stats.CustomMetrics = customStats + } + if err != nil { + customStatsErr = err + } + } + } + ref, err := c.handler.ContainerReference() if err != nil { // Ignore errors if the container is dead. @@ -444,7 +523,21 @@ func (c *containerData) updateStats() error { if err != nil { return err } - return statsErr + if statsErr != nil { + return statsErr + } + return customStatsErr +} + +func (c *containerData) updateCustomStats() (map[string]info.MetricVal, error) { + _, customStats, customStatsErr := c.collectorManager.Collect() + if customStatsErr != nil { + if !c.handler.Exists() { + return customStats, nil + } + customStatsErr = fmt.Errorf("%v, continuing to push custom stats", customStatsErr) + } + return customStats, customStatsErr } func (c *containerData) updateSubcontainers() error { diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/manager/container_test.go b/Godeps/_workspace/src/github.com/google/cadvisor/manager/container_test.go index f332dc51674..c5bcabc6b74 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/manager/container_test.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/manager/container_test.go @@ -41,7 +41,7 @@ func setupContainerData(t *testing.T, spec info.ContainerSpec) (*containerData, nil, ) memoryCache := memory.New(60, nil) - ret, err := newContainerData(containerName, memoryCache, mockHandler, nil, false, &collector.FakeCollectorManager{}) + ret, err := newContainerData(containerName, memoryCache, mockHandler, nil, false, &collector.GenericCollectorManager{}, 60*time.Second, true) if err != nil { t.Fatal(err) } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/manager/machine.go b/Godeps/_workspace/src/github.com/google/cadvisor/manager/machine.go index cbc2f186d6a..5ff60b65ba1 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/manager/machine.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/manager/machine.go @@ -17,10 +17,7 @@ package manager import ( "bytes" "flag" - "fmt" "io/ioutil" - "regexp" - "strconv" "strings" "syscall" @@ -29,193 +26,16 @@ import ( "github.com/google/cadvisor/container/docker" "github.com/google/cadvisor/fs" info "github.com/google/cadvisor/info/v1" - "github.com/google/cadvisor/utils" + "github.com/google/cadvisor/utils/cloudinfo" + "github.com/google/cadvisor/utils/machine" "github.com/google/cadvisor/utils/sysfs" "github.com/google/cadvisor/utils/sysinfo" version "github.com/google/cadvisor/version" ) -var cpuRegExp = regexp.MustCompile("processor\\t*: +([0-9]+)") -var coreRegExp = regexp.MustCompile("core id\\t*: +([0-9]+)") -var nodeRegExp = regexp.MustCompile("physical id\\t*: +([0-9]+)") -var CpuClockSpeedMHz = regexp.MustCompile("cpu MHz\\t*: +([0-9]+.[0-9]+)") -var memoryCapacityRegexp = regexp.MustCompile("MemTotal: *([0-9]+) kB") - var machineIdFilePath = flag.String("machine_id_file", "/etc/machine-id,/var/lib/dbus/machine-id", "Comma-separated list of files to check for machine-id. Use the first one that exists.") var bootIdFilePath = flag.String("boot_id_file", "/proc/sys/kernel/random/boot_id", "Comma-separated list of files to check for boot-id. Use the first one that exists.") -func getClockSpeed(procInfo []byte) (uint64, error) { - // First look through sys to find a max supported cpu frequency. - const maxFreqFile = "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq" - if utils.FileExists(maxFreqFile) { - val, err := ioutil.ReadFile(maxFreqFile) - if err != nil { - return 0, err - } - var maxFreq uint64 - n, err := fmt.Sscanf(string(val), "%d", &maxFreq) - if err != nil || n != 1 { - return 0, fmt.Errorf("could not parse frequency %q", val) - } - return maxFreq, nil - } - // Fall back to /proc/cpuinfo - matches := CpuClockSpeedMHz.FindSubmatch(procInfo) - if len(matches) != 2 { - //Check if we are running on Power systems which have a different format - CpuClockSpeedMHz, _ = regexp.Compile("clock\\t*: +([0-9]+.[0-9]+)MHz") - matches = CpuClockSpeedMHz.FindSubmatch(procInfo) - if len(matches) != 2 { - return 0, fmt.Errorf("could not detect clock speed from output: %q", string(procInfo)) - } - } - speed, err := strconv.ParseFloat(string(matches[1]), 64) - if err != nil { - return 0, err - } - // Convert to kHz - return uint64(speed * 1000), nil -} - -func getMemoryCapacity(b []byte) (int64, error) { - matches := memoryCapacityRegexp.FindSubmatch(b) - if len(matches) != 2 { - return -1, fmt.Errorf("failed to find memory capacity in output: %q", string(b)) - } - m, err := strconv.ParseInt(string(matches[1]), 10, 64) - if err != nil { - return -1, err - } - - // Convert to bytes. - return m * 1024, err -} - -func extractValue(s string, r *regexp.Regexp) (bool, int, error) { - matches := r.FindSubmatch([]byte(s)) - if len(matches) == 2 { - val, err := strconv.ParseInt(string(matches[1]), 10, 32) - if err != nil { - return true, -1, err - } - return true, int(val), nil - } - return false, -1, nil -} - -func findNode(nodes []info.Node, id int) (bool, int) { - for i, n := range nodes { - if n.Id == id { - return true, i - } - } - return false, -1 -} - -func addNode(nodes *[]info.Node, id int) (int, error) { - var idx int - if id == -1 { - // Some VMs don't fill topology data. Export single package. - id = 0 - } - - ok, idx := findNode(*nodes, id) - if !ok { - // New node - node := info.Node{Id: id} - // Add per-node memory information. - meminfo := fmt.Sprintf("/sys/devices/system/node/node%d/meminfo", id) - out, err := ioutil.ReadFile(meminfo) - // Ignore if per-node info is not available. - if err == nil { - m, err := getMemoryCapacity(out) - if err != nil { - return -1, err - } - node.Memory = uint64(m) - } - *nodes = append(*nodes, node) - idx = len(*nodes) - 1 - } - return idx, nil -} - -func getTopology(sysFs sysfs.SysFs, cpuinfo string) ([]info.Node, int, error) { - nodes := []info.Node{} - numCores := 0 - lastThread := -1 - lastCore := -1 - lastNode := -1 - for _, line := range strings.Split(cpuinfo, "\n") { - ok, val, err := extractValue(line, cpuRegExp) - if err != nil { - return nil, -1, fmt.Errorf("could not parse cpu info from %q: %v", line, err) - } - if ok { - thread := val - numCores++ - if lastThread != -1 { - // New cpu section. Save last one. - nodeIdx, err := addNode(&nodes, lastNode) - if err != nil { - return nil, -1, fmt.Errorf("failed to add node %d: %v", lastNode, err) - } - nodes[nodeIdx].AddThread(lastThread, lastCore) - lastCore = -1 - lastNode = -1 - } - lastThread = thread - } - ok, val, err = extractValue(line, coreRegExp) - if err != nil { - return nil, -1, fmt.Errorf("could not parse core info from %q: %v", line, err) - } - if ok { - lastCore = val - } - ok, val, err = extractValue(line, nodeRegExp) - if err != nil { - return nil, -1, fmt.Errorf("could not parse node info from %q: %v", line, err) - } - if ok { - lastNode = val - } - } - nodeIdx, err := addNode(&nodes, lastNode) - if err != nil { - return nil, -1, fmt.Errorf("failed to add node %d: %v", lastNode, err) - } - nodes[nodeIdx].AddThread(lastThread, lastCore) - if numCores < 1 { - return nil, numCores, fmt.Errorf("could not detect any cores") - } - for idx, node := range nodes { - caches, err := sysinfo.GetCacheInfo(sysFs, node.Cores[0].Threads[0]) - if err != nil { - glog.Errorf("failed to get cache information for node %d: %v", node.Id, err) - continue - } - numThreadsPerCore := len(node.Cores[0].Threads) - numThreadsPerNode := len(node.Cores) * numThreadsPerCore - for _, cache := range caches { - c := info.Cache{ - Size: cache.Size, - Level: cache.Level, - Type: cache.Type, - } - if cache.Cpus == numThreadsPerNode && cache.Level > 2 { - // Add a node-level cache. - nodes[idx].AddNodeCache(c) - } else if cache.Cpus == numThreadsPerCore { - // Add to each core. - nodes[idx].AddPerCoreCache(c) - } - // Ignore unknown caches. - } - } - return nodes, numCores, nil -} - func getInfoFromFiles(filePaths string) string { if len(filePaths) == 0 { return "" @@ -232,18 +52,12 @@ func getInfoFromFiles(filePaths string) string { func getMachineInfo(sysFs sysfs.SysFs, fsInfo fs.FsInfo) (*info.MachineInfo, error) { cpuinfo, err := ioutil.ReadFile("/proc/cpuinfo") - clockSpeed, err := getClockSpeed(cpuinfo) + clockSpeed, err := machine.GetClockSpeed(cpuinfo) if err != nil { return nil, err } - // Get the amount of usable memory from /proc/meminfo. - out, err := ioutil.ReadFile("/proc/meminfo") - if err != nil { - return nil, err - } - - memoryCapacity, err := getMemoryCapacity(out) + memoryCapacity, err := machine.GetMachineMemoryCapacity() if err != nil { return nil, err } @@ -263,7 +77,7 @@ func getMachineInfo(sysFs sysfs.SysFs, fsInfo fs.FsInfo) (*info.MachineInfo, err glog.Errorf("Failed to get network devices: %v", err) } - topology, numCores, err := getTopology(sysFs, string(cpuinfo)) + topology, numCores, err := machine.GetTopology(sysFs, string(cpuinfo)) if err != nil { glog.Errorf("Failed to get topology information: %v", err) } @@ -273,6 +87,10 @@ func getMachineInfo(sysFs sysfs.SysFs, fsInfo fs.FsInfo) (*info.MachineInfo, err glog.Errorf("Failed to get system UUID: %v", err) } + realCloudInfo := cloudinfo.NewRealCloudInfo() + cloudProvider := realCloudInfo.GetCloudProvider() + instanceType := realCloudInfo.GetInstanceType() + machineInfo := &info.MachineInfo{ NumCores: numCores, CpuFrequency: clockSpeed, @@ -283,6 +101,8 @@ func getMachineInfo(sysFs sysfs.SysFs, fsInfo fs.FsInfo) (*info.MachineInfo, err MachineID: getInfoFromFiles(*machineIdFilePath), SystemUUID: systemUUID, BootID: getInfoFromFiles(*bootIdFilePath), + CloudProvider: cloudProvider, + InstanceType: instanceType, } for _, fs := range filesystems { diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager.go b/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager.go index 62255147f22..751acb97071 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager.go @@ -114,7 +114,7 @@ type Manager interface { } // New takes a memory storage and returns a new manager. -func New(memoryCache *memory.InMemoryCache, sysfs sysfs.SysFs) (Manager, error) { +func New(memoryCache *memory.InMemoryCache, sysfs sysfs.SysFs, maxHousekeepingInterval time.Duration, allowDynamicHousekeeping bool) (Manager, error) { if memoryCache == nil { return nil, fmt.Errorf("manager requires memory storage") } @@ -139,13 +139,15 @@ func New(memoryCache *memory.InMemoryCache, sysfs sysfs.SysFs) (Manager, error) inHostNamespace = true } newManager := &manager{ - containers: make(map[namespacedContainerName]*containerData), - quitChannels: make([]chan error, 0, 2), - memoryCache: memoryCache, - fsInfo: fsInfo, - cadvisorContainer: selfContainer, - inHostNamespace: inHostNamespace, - startupTime: time.Now(), + containers: make(map[namespacedContainerName]*containerData), + quitChannels: make([]chan error, 0, 2), + memoryCache: memoryCache, + fsInfo: fsInfo, + cadvisorContainer: selfContainer, + inHostNamespace: inHostNamespace, + startupTime: time.Now(), + maxHousekeepingInterval: maxHousekeepingInterval, + allowDynamicHousekeeping: allowDynamicHousekeeping, } machineInfo, err := getMachineInfo(sysfs, fsInfo) @@ -176,19 +178,21 @@ type namespacedContainerName struct { } type manager struct { - containers map[namespacedContainerName]*containerData - containersLock sync.RWMutex - memoryCache *memory.InMemoryCache - fsInfo fs.FsInfo - machineInfo info.MachineInfo - versionInfo info.VersionInfo - quitChannels []chan error - cadvisorContainer string - inHostNamespace bool - dockerContainersRegexp *regexp.Regexp - loadReader cpuload.CpuLoadReader - eventHandler events.EventManager - startupTime time.Time + containers map[namespacedContainerName]*containerData + containersLock sync.RWMutex + memoryCache *memory.InMemoryCache + fsInfo fs.FsInfo + machineInfo info.MachineInfo + versionInfo info.VersionInfo + quitChannels []chan error + cadvisorContainer string + inHostNamespace bool + dockerContainersRegexp *regexp.Regexp + loadReader cpuload.CpuLoadReader + eventHandler events.EventManager + startupTime time.Time + maxHousekeepingInterval time.Duration + allowDynamicHousekeeping bool } // Start the container manager. @@ -371,12 +375,13 @@ func (self *manager) GetContainerSpec(containerName string, options v2.RequestOp func (self *manager) getV2Spec(cinfo *containerInfo) v2.ContainerSpec { specV1 := self.getAdjustedSpec(cinfo) specV2 := v2.ContainerSpec{ - CreationTime: specV1.CreationTime, - HasCpu: specV1.HasCpu, - HasMemory: specV1.HasMemory, - HasFilesystem: specV1.HasFilesystem, - HasNetwork: specV1.HasNetwork, - HasDiskIo: specV1.HasDiskIo, + CreationTime: specV1.CreationTime, + HasCpu: specV1.HasCpu, + HasMemory: specV1.HasMemory, + HasFilesystem: specV1.HasFilesystem, + HasNetwork: specV1.HasNetwork, + HasDiskIo: specV1.HasDiskIo, + HasCustomMetrics: specV1.HasCustomMetrics, } if specV1.HasCpu { specV2.Cpu.Limit = specV1.Cpu.Limit @@ -388,6 +393,9 @@ func (self *manager) getV2Spec(cinfo *containerInfo) v2.ContainerSpec { specV2.Memory.Reservation = specV1.Memory.Reservation specV2.Memory.SwapLimit = specV1.Memory.SwapLimit } + if specV1.HasCustomMetrics { + specV2.CustomMetrics = specV1.CustomMetrics + } specV2.Aliases = cinfo.Aliases specV2.Namespace = cinfo.Namespace return specV2 @@ -689,6 +697,28 @@ func (m *manager) GetProcessList(containerName string, options v2.RequestOptions return ps, nil } +func (m *manager) registerCollectors(collectorConfigs map[string]string, cont *containerData) error { + for k, v := range collectorConfigs { + configFile, err := cont.ReadFile(v, m.inHostNamespace) + if err != nil { + return fmt.Errorf("failed to read config file %q for config %q, container %q: %v", k, v, cont.info.Name, err) + } + glog.V(3).Infof("Got config from %q: %q", v, configFile) + + newCollector, err := collector.NewCollector(k, configFile) + if err != nil { + glog.Infof("failed to create collector for container %q, config %q: %v", cont.info.Name, k, err) + return err + } + err = cont.collectorManager.RegisterCollector(newCollector) + if err != nil { + glog.Infof("failed to register collector for container %q, config %q: %v", cont.info.Name, k, err) + return err + } + } + return nil +} + // Create a container. func (m *manager) createContainer(containerName string) error { handler, accept, err := container.NewContainerHandler(containerName) @@ -700,17 +730,26 @@ func (m *manager) createContainer(containerName string) error { glog.V(4).Infof("ignoring container %q", containerName) return nil } - // TODO(vmarmol): Register collectors. collectorManager, err := collector.NewCollectorManager() if err != nil { return err } + logUsage := *logCadvisorUsage && containerName == m.cadvisorContainer - cont, err := newContainerData(containerName, m.memoryCache, handler, m.loadReader, logUsage, collectorManager) + cont, err := newContainerData(containerName, m.memoryCache, handler, m.loadReader, logUsage, collectorManager, m.maxHousekeepingInterval, m.allowDynamicHousekeeping) if err != nil { return err } + // Add collectors + labels := handler.GetContainerLabels() + collectorConfigs := collector.GetCollectorConfigs(labels) + err = m.registerCollectors(collectorConfigs, cont) + if err != nil { + glog.Infof("failed to register collectors for %q: %v", containerName, err) + return err + } + // Add to the containers map. alreadyExists := func() bool { m.containersLock.Lock() diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager_test.go b/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager_test.go index 6cfdc6373f3..b1600b88aa1 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager_test.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/manager/manager_test.go @@ -53,7 +53,7 @@ func createManagerAndAddContainers( spec, nil, ).Once() - cont, err := newContainerData(name, memoryCache, mockHandler, nil, false, &collector.FakeCollectorManager{}) + cont, err := newContainerData(name, memoryCache, mockHandler, nil, false, &collector.GenericCollectorManager{}, 60*time.Second, true) if err != nil { t.Fatal(err) } @@ -205,7 +205,7 @@ func TestDockerContainersInfo(t *testing.T) { } func TestNewNilManager(t *testing.T) { - _, err := New(nil, nil) + _, err := New(nil, nil, 60*time.Second, true) if err == nil { t.Fatalf("Expected nil manager to return error") } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/pages/containers.go b/Godeps/_workspace/src/github.com/google/cadvisor/pages/containers.go index 4a83c4835e9..394c5d1628c 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/pages/containers.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/pages/containers.go @@ -18,7 +18,6 @@ package pages import ( "fmt" "html/template" - "math" "net/http" "net/url" "path" @@ -149,15 +148,19 @@ func toMegabytes(bytes uint64) float64 { return float64(bytes) / (1 << 20) } +// Size after which we consider memory to be "unlimited". This is not +// MaxInt64 due to rounding by the kernel. +const maxMemorySize = uint64(1 << 62) + func printSize(bytes uint64) string { - if bytes >= math.MaxInt64 { + if bytes >= maxMemorySize { return "unlimited" } return ByteSize(bytes).Size() } func printUnit(bytes uint64) string { - if bytes >= math.MaxInt64 { + if bytes >= maxMemorySize { return "" } return ByteSize(bytes).Unit() @@ -229,7 +232,7 @@ func serveContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL) e data := &pageData{ DisplayName: displayName, - ContainerName: cont.Name, + ContainerName: escapeContainerName(cont.Name), ParentContainers: parentContainers, Subcontainers: subcontainerLinks, Spec: cont.Spec, diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/pages/docker.go b/Godeps/_workspace/src/github.com/google/cadvisor/pages/docker.go index d02f1ceebdb..599cb0ac81b 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/pages/docker.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/pages/docker.go @@ -130,7 +130,7 @@ func serveDockerPage(m manager.Manager, w http.ResponseWriter, u *url.URL) error } data = &pageData{ DisplayName: displayName, - ContainerName: cont.Name, + ContainerName: escapeContainerName(cont.Name), ParentContainers: parentContainers, Spec: cont.Spec, Stats: cont.Stats, diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/pages/pages.go b/Godeps/_workspace/src/github.com/google/cadvisor/pages/pages.go index fed0401f27f..662be0a304f 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/pages/pages.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/pages/pages.go @@ -18,6 +18,7 @@ import ( "fmt" "html/template" "net/http" + "net/url" "strings" auth "github.com/abbot/go-http-auth" @@ -159,3 +160,12 @@ func getContainerDisplayName(cont info.ContainerReference) string { return displayName } + +// Escape the non-path characters on a container name. +func escapeContainerName(containerName string) string { + parts := strings.Split(containerName, "/") + for i := range parts { + parts[i] = url.QueryEscape(parts[i]) + } + return strings.Join(parts, "/") +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/storage/redis/redis.go b/Godeps/_workspace/src/github.com/google/cadvisor/storage/redis/redis.go index 5d1e4ce480f..d66190c1a10 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/storage/redis/redis.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/storage/redis/redis.go @@ -44,8 +44,8 @@ func (self *redisStorage) defaultReadyToFlush() bool { return time.Since(self.lastWrite) >= self.bufferDuration } -//We must add some defaut params (for example: MachineName,ContainerName...)because containerStats do not include them -func (self *redisStorage) containerStatsAndDefautValues(ref info.ContainerReference, stats *info.ContainerStats) *detailSpec { +//We must add some default params (for example: MachineName,ContainerName...)because containerStats do not include them +func (self *redisStorage) containerStatsAndDefaultValues(ref info.ContainerReference, stats *info.ContainerStats) *detailSpec { timestamp := stats.Timestamp.UnixNano() / 1E3 var containerName string if len(ref.Aliases) > 0 { @@ -72,8 +72,8 @@ func (self *redisStorage) AddStats(ref info.ContainerReference, stats *info.Cont // AddStats will be invoked simultaneously from multiple threads and only one of them will perform a write. self.lock.Lock() defer self.lock.Unlock() - // Add some defaut params based on containerStats - detail := self.containerStatsAndDefautValues(ref, stats) + // Add some default params based on containerStats + detail := self.containerStatsAndDefaultValues(ref, stats) //To json b, _ := json.Marshal(detail) if self.readyToFlush() { diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/client.go b/Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/client/client.go similarity index 61% rename from Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/client.go rename to Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/client/client.go index ffef57ff469..958468ad554 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/client.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/client/client.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package statsd +package client import ( "fmt" @@ -22,8 +22,9 @@ import ( ) type Client struct { - HostPort string - conn net.Conn + HostPort string + Namespace string + conn net.Conn } func (self *Client) Open() error { @@ -36,38 +37,28 @@ func (self *Client) Open() error { return nil } -func (self *Client) Close() { +func (self *Client) Close() error { self.conn.Close() + self.conn = nil + return nil } -func (self *Client) UpdateGauge(name, value string) error { - stats := make(map[string]string) - val := fmt.Sprintf("%s|g", value) - stats[name] = val - if err := self.send(stats); err != nil { +// Simple send to statsd daemon without sampling. +func (self *Client) Send(namespace, containerName, key string, value uint64) error { + // only send counter value + formatted := fmt.Sprintf("%s.%s.%s:%d|g", namespace, containerName, key, value) + _, err := fmt.Fprintf(self.conn, formatted) + if err != nil { + glog.V(3).Infof("failed to send data %q: %v", formatted, err) return err } return nil } -// Simple send to statsd daemon without sampling. -func (self *Client) send(data map[string]string) error { - for k, v := range data { - formatted := fmt.Sprintf("%s:%s", k, v) - _, err := fmt.Fprintf(self.conn, formatted) - if err != nil { - glog.V(3).Infof("failed to send data %q: %v", formatted, err) - // return on first error. - return err - } - } - return nil -} - func New(hostPort string) (*Client, error) { - client := Client{HostPort: hostPort} - if err := client.Open(); err != nil { + Client := Client{HostPort: hostPort} + if err := Client.Open(); err != nil { return nil, err } - return &client, nil + return &Client, nil } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/statsd.go b/Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/statsd.go new file mode 100644 index 00000000000..0b4ce9f4db8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/storage/statsd/statsd.go @@ -0,0 +1,127 @@ +// Copyright 2015 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 statsd + +import ( + info "github.com/google/cadvisor/info/v1" + client "github.com/google/cadvisor/storage/statsd/client" +) + +type statsdStorage struct { + client *client.Client + Namespace string +} + +const ( + colCpuCumulativeUsage string = "cpu_cumulative_usage" + // Memory Usage + colMemoryUsage string = "memory_usage" + // Working set size + colMemoryWorkingSet string = "memory_working_set" + // Cumulative count of bytes received. + colRxBytes string = "rx_bytes" + // Cumulative count of receive errors encountered. + colRxErrors string = "rx_errors" + // Cumulative count of bytes transmitted. + colTxBytes string = "tx_bytes" + // Cumulative count of transmit errors encountered. + colTxErrors string = "tx_errors" + // Filesystem summary + colFsSummary = "fs_summary" + // Filesystem limit. + colFsLimit = "fs_limit" + // Filesystem usage. + colFsUsage = "fs_usage" +) + +func (self *statsdStorage) containerStatsToValues( + stats *info.ContainerStats, +) (series map[string]uint64) { + series = make(map[string]uint64) + + // Cumulative Cpu Usage + series[colCpuCumulativeUsage] = stats.Cpu.Usage.Total + + // Memory Usage + series[colMemoryUsage] = stats.Memory.Usage + + // Working set size + series[colMemoryWorkingSet] = stats.Memory.WorkingSet + + // Network stats. + series[colRxBytes] = stats.Network.RxBytes + series[colRxErrors] = stats.Network.RxErrors + series[colTxBytes] = stats.Network.TxBytes + series[colTxErrors] = stats.Network.TxErrors + + return series +} + +func (self *statsdStorage) containerFsStatsToValues( + series *map[string]uint64, + stats *info.ContainerStats, +) { + for _, fsStat := range stats.Filesystem { + // Summary stats. + (*series)[colFsSummary+"."+colFsLimit] += fsStat.Limit + (*series)[colFsSummary+"."+colFsUsage] += fsStat.Usage + + // Per device stats. + (*series)[fsStat.Device+"."+colFsLimit] = fsStat.Limit + (*series)[fsStat.Device+"."+colFsUsage] = fsStat.Usage + } +} + +//Push the data into redis +func (self *statsdStorage) AddStats(ref info.ContainerReference, stats *info.ContainerStats) error { + if stats == nil { + return nil + } + + var containerName string + if len(ref.Aliases) > 0 { + containerName = ref.Aliases[0] + } else { + containerName = ref.Name + } + + series := self.containerStatsToValues(stats) + self.containerFsStatsToValues(&series, stats) + for key, value := range series { + err := self.client.Send(self.Namespace, containerName, key, value) + if err != nil { + return err + } + } + return nil +} + +func (self *statsdStorage) Close() error { + self.client.Close() + self.client = nil + return nil +} + +func New(namespace, hostPort string) (*statsdStorage, error) { + statsdClient, err := client.New(hostPort) + if err != nil { + return nil, err + } + statsdStorage := &statsdStorage{ + client: statsdClient, + Namespace: namespace, + } + return statsdStorage, nil +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles.go b/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles.go index 1893a65c0c0..de92bc3a251 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles.go @@ -28,20 +28,23 @@ const secondsToMilliSeconds = 1000 const milliSecondsToNanoSeconds = 1000000 const secondsToNanoSeconds = secondsToMilliSeconds * milliSecondsToNanoSeconds -type uint64Slice []uint64 +type Uint64Slice []uint64 -func (a uint64Slice) Len() int { return len(a) } -func (a uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] } +func (a Uint64Slice) Len() int { return len(a) } +func (a Uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Uint64Slice) Less(i, j int) bool { return a[i] < a[j] } -// Get 90th percentile of the provided samples. Round to integer. -func (self uint64Slice) Get90Percentile() uint64 { +// Get percentile of the provided samples. Round to integer. +func (self Uint64Slice) GetPercentile(d float64) uint64 { + if d < 0.0 || d > 1.0 { + return 0 + } count := self.Len() if count == 0 { return 0 } sort.Sort(self) - n := float64(0.9 * (float64(count) + 1)) + n := float64(d * (float64(count) + 1)) idx, frac := math.Modf(n) index := int(idx) percentile := float64(self[index-1]) @@ -71,7 +74,7 @@ func (self *mean) Add(value uint64) { type resource struct { // list of samples being tracked. - samples uint64Slice + samples Uint64Slice // average from existing samples. mean mean // maximum value seen so far in the added samples. @@ -94,27 +97,31 @@ func (self *resource) Add(p info.Percentiles) { // Add a single sample. Internally, we convert it to a fake percentile sample. func (self *resource) AddSample(val uint64) { sample := info.Percentiles{ - Present: true, - Mean: val, - Max: val, - Ninety: val, + Present: true, + Mean: val, + Max: val, + Fifty: val, + Ninety: val, + NinetyFive: val, } self.Add(sample) } // Get max, average, and 90p from existing samples. -func (self *resource) GetPercentile() info.Percentiles { +func (self *resource) GetAllPercentiles() info.Percentiles { p := info.Percentiles{} p.Mean = uint64(self.mean.Mean) p.Max = self.max - p.Ninety = self.samples.Get90Percentile() + p.Fifty = self.samples.GetPercentile(0.5) + p.Ninety = self.samples.GetPercentile(0.9) + p.NinetyFive = self.samples.GetPercentile(0.95) p.Present = true return p } func NewResource(size int) *resource { return &resource{ - samples: make(uint64Slice, 0, size), + samples: make(Uint64Slice, 0, size), mean: mean{count: 0, Mean: 0}, } } @@ -128,8 +135,8 @@ func GetDerivedPercentiles(stats []*info.Usage) info.Usage { memory.Add(stat.Memory) } usage := info.Usage{} - usage.Cpu = cpu.GetPercentile() - usage.Memory = memory.GetPercentile() + usage.Cpu = cpu.GetAllPercentiles() + usage.Memory = memory.GetAllPercentiles() return usage } @@ -183,7 +190,7 @@ func GetMinutePercentiles(stats []*secondSample) info.Usage { percent := getPercentComplete(stats) return info.Usage{ PercentComplete: percent, - Cpu: cpu.GetPercentile(), - Memory: memory.GetPercentile(), + Cpu: cpu.GetAllPercentiles(), + Memory: memory.GetAllPercentiles(), } } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles_test.go b/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles_test.go index 53b6a29f2c2..4dbe3665d35 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles_test.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/summary/percentiles_test.go @@ -23,25 +23,29 @@ import ( const Nanosecond = 1000000000 -func Test90Percentile(t *testing.T) { +func assertPercentile(t *testing.T, s Uint64Slice, f float64, want uint64) { + if got := s.GetPercentile(f); got != want { + t.Errorf("GetPercentile(%f) is %d, should be %d.", f, got, want) + } +} + +func TestPercentile(t *testing.T) { N := 100 - stats := make(uint64Slice, 0, N) + s := make(Uint64Slice, 0, N) for i := N; i > 0; i-- { - stats = append(stats, uint64(i)) + s = append(s, uint64(i)) } - p := stats.Get90Percentile() - if p != 90 { - t.Errorf("90th percentile is %d, should be 90.", p) - } - // 90p should be between 94 and 95. Promoted to 95. + assertPercentile(t, s, 0.2, 20) + assertPercentile(t, s, 0.7, 70) + assertPercentile(t, s, 0.9, 90) N = 105 for i := 101; i <= N; i++ { - stats = append(stats, uint64(i)) - } - p = stats.Get90Percentile() - if p != 95 { - t.Errorf("90th percentile is %d, should be 95.", p) + s = append(s, uint64(i)) } + // 90p should be between 94 and 95. Promoted to 95. + assertPercentile(t, s, 0.2, 21) + assertPercentile(t, s, 0.7, 74) + assertPercentile(t, s, 0.9, 95) } func TestMean(t *testing.T) { @@ -74,19 +78,23 @@ func TestAggregates(t *testing.T) { usage := GetMinutePercentiles(stats) // Cpu mean, max, and 90p should all be 1000 ms/s. cpuExpected := info.Percentiles{ - Present: true, - Mean: 1000, - Max: 1000, - Ninety: 1000, + Present: true, + Mean: 1000, + Max: 1000, + Fifty: 1000, + Ninety: 1000, + NinetyFive: 1000, } if usage.Cpu != cpuExpected { t.Errorf("cpu stats are %+v. Expected %+v", usage.Cpu, cpuExpected) } memExpected := info.Percentiles{ - Present: true, - Mean: 50 * 1024, - Max: 99 * 1024, - Ninety: 90 * 1024, + Present: true, + Mean: 50 * 1024, + Max: 99 * 1024, + Fifty: 50 * 1024, + Ninety: 90 * 1024, + NinetyFive: 95 * 1024, } if usage.Memory != memExpected { t.Errorf("memory stats are mean %+v. Expected %+v", usage.Memory, memExpected) @@ -119,19 +127,23 @@ func TestSamplesCloseInTimeIgnored(t *testing.T) { usage := GetMinutePercentiles(stats) // Cpu mean, max, and 90p should all be 1000 ms/s. All high-value samples are discarded. cpuExpected := info.Percentiles{ - Present: true, - Mean: 1000, - Max: 1000, - Ninety: 1000, + Present: true, + Mean: 1000, + Max: 1000, + Fifty: 1000, + Ninety: 1000, + NinetyFive: 1000, } if usage.Cpu != cpuExpected { t.Errorf("cpu stats are %+v. Expected %+v", usage.Cpu, cpuExpected) } memExpected := info.Percentiles{ - Present: true, - Mean: 50 * 1024, - Max: 99 * 1024, - Ninety: 90 * 1024, + Present: true, + Mean: 50 * 1024, + Max: 99 * 1024, + Fifty: 50 * 1024, + Ninety: 90 * 1024, + NinetyFive: 95 * 1024, } if usage.Memory != memExpected { t.Errorf("memory stats are mean %+v. Expected %+v", usage.Memory, memExpected) @@ -146,35 +158,43 @@ func TestDerivedStats(t *testing.T) { s := &info.Usage{ PercentComplete: 100, Cpu: info.Percentiles{ - Present: true, - Mean: i * Nanosecond, - Max: i * Nanosecond, - Ninety: i * Nanosecond, + Present: true, + Mean: i * Nanosecond, + Max: i * Nanosecond, + Fifty: i * Nanosecond, + Ninety: i * Nanosecond, + NinetyFive: i * Nanosecond, }, Memory: info.Percentiles{ - Present: true, - Mean: i * 1024, - Max: i * 1024, - Ninety: i * 1024, + Present: true, + Mean: i * 1024, + Max: i * 1024, + Fifty: i * 1024, + Ninety: i * 1024, + NinetyFive: i * 1024, }, } stats = append(stats, s) } usage := GetDerivedPercentiles(stats) cpuExpected := info.Percentiles{ - Present: true, - Mean: 50 * Nanosecond, - Max: 99 * Nanosecond, - Ninety: 90 * Nanosecond, + Present: true, + Mean: 50 * Nanosecond, + Max: 99 * Nanosecond, + Fifty: 50 * Nanosecond, + Ninety: 90 * Nanosecond, + NinetyFive: 95 * Nanosecond, } if usage.Cpu != cpuExpected { t.Errorf("cpu stats are %+v. Expected %+v", usage.Cpu, cpuExpected) } memExpected := info.Percentiles{ - Present: true, - Mean: 50 * 1024, - Max: 99 * 1024, - Ninety: 90 * 1024, + Present: true, + Mean: 50 * 1024, + Max: 99 * 1024, + Fifty: 50 * 1024, + Ninety: 90 * 1024, + NinetyFive: 95 * 1024, } if usage.Memory != memExpected { t.Errorf("memory stats are mean %+v. Expected %+v", usage.Memory, memExpected) diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/cloudinfo.go b/Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/cloudinfo.go new file mode 100644 index 00000000000..e073fae622b --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/cloudinfo.go @@ -0,0 +1,87 @@ +// Copyright 2015 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. + +// Get information about the cloud provider (if any) cAdvisor is running on. + +package cloudinfo + +import ( + info "github.com/google/cadvisor/info/v1" +) + +type CloudInfo interface { + GetCloudProvider() info.CloudProvider + GetInstanceType() info.InstanceType +} + +type realCloudInfo struct { + cloudProvider info.CloudProvider + instanceType info.InstanceType +} + +func NewRealCloudInfo() CloudInfo { + cloudProvider := detectCloudProvider() + instanceType := detectInstanceType(cloudProvider) + return &realCloudInfo{ + cloudProvider: cloudProvider, + instanceType: instanceType, + } +} + +func (self *realCloudInfo) GetCloudProvider() info.CloudProvider { + return self.cloudProvider +} + +func (self *realCloudInfo) GetInstanceType() info.InstanceType { + return self.instanceType +} + +func detectCloudProvider() info.CloudProvider { + switch { + case onGCE(): + return info.GCE + case onAWS(): + return info.AWS + case onBaremetal(): + return info.Baremetal + } + return info.UnkownProvider +} + +func detectInstanceType(cloudProvider info.CloudProvider) info.InstanceType { + switch cloudProvider { + case info.GCE: + return getGceInstanceType() + case info.AWS: + return getAwsInstanceType() + case info.Baremetal: + return info.NoInstance + } + return info.UnknownInstance +} + +//TODO: Implement method. +func onAWS() bool { + return false +} + +//TODO: Implement method. +func getAwsInstanceType() info.InstanceType { + return info.UnknownInstance +} + +//TODO: Implement method. +func onBaremetal() bool { + return false +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/gce.go b/Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/gce.go new file mode 100644 index 00000000000..2ea27da2f0b --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/utils/cloudinfo/gce.go @@ -0,0 +1,36 @@ +// Copyright 2015 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 cloudinfo + +import ( + "strings" + + "github.com/GoogleCloudPlatform/gcloud-golang/compute/metadata" + info "github.com/google/cadvisor/info/v1" +) + +func onGCE() bool { + return metadata.OnGCE() +} + +func getGceInstanceType() info.InstanceType { + machineType, err := metadata.Get("instance/machine-type") + if err != nil { + return info.UnknownInstance + } + + responseParts := strings.Split(machineType, "/") // Extract the instance name from the machine type. + return info.InstanceType(responseParts[len(responseParts)-1]) +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/utils/fs/mockfs/mockfs.go b/Godeps/_workspace/src/github.com/google/cadvisor/utils/fs/mockfs/mockfs.go index 93f08a686e4..cc3d615ee44 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/utils/fs/mockfs/mockfs.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/utils/fs/mockfs/mockfs.go @@ -18,7 +18,7 @@ package mockfs import ( - gomock "code.google.com/p/gomock/gomock" + gomock "github.com/golang/mock/gomock" fs "github.com/google/cadvisor/utils/fs" ) diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/machine.go b/Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/machine.go new file mode 100644 index 00000000000..4d4ce23e345 --- /dev/null +++ b/Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/machine.go @@ -0,0 +1,243 @@ +// Copyright 2015 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 machine + +import ( + "fmt" + "io/ioutil" + "regexp" + "strconv" + "strings" + + "github.com/golang/glog" + info "github.com/google/cadvisor/info/v1" + "github.com/google/cadvisor/utils" + "github.com/google/cadvisor/utils/sysfs" + "github.com/google/cadvisor/utils/sysinfo" +) + +// The utils/machine package contains functions that extract machine-level specs. + +var cpuRegExp = regexp.MustCompile("processor\\t*: +([0-9]+)") +var coreRegExp = regexp.MustCompile("core id\\t*: +([0-9]+)") +var nodeRegExp = regexp.MustCompile("physical id\\t*: +([0-9]+)") +var CpuClockSpeedMHz = regexp.MustCompile("cpu MHz\\t*: +([0-9]+.[0-9]+)") +var memoryCapacityRegexp = regexp.MustCompile("MemTotal: *([0-9]+) kB") +var swapCapacityRegexp = regexp.MustCompile("SwapTotal: *([0-9]+) kB") + +// GetClockSpeed returns the CPU clock speed, given a []byte formatted as the /proc/cpuinfo file. +func GetClockSpeed(procInfo []byte) (uint64, error) { + // First look through sys to find a max supported cpu frequency. + const maxFreqFile = "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq" + if utils.FileExists(maxFreqFile) { + val, err := ioutil.ReadFile(maxFreqFile) + if err != nil { + return 0, err + } + var maxFreq uint64 + n, err := fmt.Sscanf(string(val), "%d", &maxFreq) + if err != nil || n != 1 { + return 0, fmt.Errorf("could not parse frequency %q", val) + } + return maxFreq, nil + } + // Fall back to /proc/cpuinfo + matches := CpuClockSpeedMHz.FindSubmatch(procInfo) + if len(matches) != 2 { + //Check if we are running on Power systems which have a different format + CpuClockSpeedMHz, _ = regexp.Compile("clock\\t*: +([0-9]+.[0-9]+)MHz") + matches = CpuClockSpeedMHz.FindSubmatch(procInfo) + if len(matches) != 2 { + return 0, fmt.Errorf("could not detect clock speed from output: %q", string(procInfo)) + } + } + speed, err := strconv.ParseFloat(string(matches[1]), 64) + if err != nil { + return 0, err + } + // Convert to kHz + return uint64(speed * 1000), nil +} + +// GetMachineMemoryCapacity returns the machine's total memory from /proc/meminfo. +// Returns the total memory capacity as an int64 (number of bytes). +func GetMachineMemoryCapacity() (int64, error) { + out, err := ioutil.ReadFile("/proc/meminfo") + if err != nil { + return 0, err + } + + memoryCapacity, err := parseCapacity(out, memoryCapacityRegexp) + if err != nil { + return 0, err + } + return memoryCapacity, err +} + +// GetMachineSwapCapacity returns the machine's total swap from /proc/meminfo. +// Returns the total swap capacity as an int64 (number of bytes). +func GetMachineSwapCapacity() (int64, error) { + out, err := ioutil.ReadFile("/proc/meminfo") + if err != nil { + return 0, err + } + + swapCapacity, err := parseCapacity(out, swapCapacityRegexp) + if err != nil { + return 0, err + } + return swapCapacity, err +} + +// parseCapacity matches a Regexp in a []byte, returning the resulting value in bytes. +// Assumes that the value matched by the Regexp is in KB. +func parseCapacity(b []byte, r *regexp.Regexp) (int64, error) { + matches := r.FindSubmatch(b) + if len(matches) != 2 { + return -1, fmt.Errorf("failed to match regexp in output: %q", string(b)) + } + m, err := strconv.ParseInt(string(matches[1]), 10, 64) + if err != nil { + return -1, err + } + + // Convert to bytes. + return m * 1024, err +} + +func GetTopology(sysFs sysfs.SysFs, cpuinfo string) ([]info.Node, int, error) { + nodes := []info.Node{} + numCores := 0 + lastThread := -1 + lastCore := -1 + lastNode := -1 + for _, line := range strings.Split(cpuinfo, "\n") { + ok, val, err := extractValue(line, cpuRegExp) + if err != nil { + return nil, -1, fmt.Errorf("could not parse cpu info from %q: %v", line, err) + } + if ok { + thread := val + numCores++ + if lastThread != -1 { + // New cpu section. Save last one. + nodeIdx, err := addNode(&nodes, lastNode) + if err != nil { + return nil, -1, fmt.Errorf("failed to add node %d: %v", lastNode, err) + } + nodes[nodeIdx].AddThread(lastThread, lastCore) + lastCore = -1 + lastNode = -1 + } + lastThread = thread + } + ok, val, err = extractValue(line, coreRegExp) + if err != nil { + return nil, -1, fmt.Errorf("could not parse core info from %q: %v", line, err) + } + if ok { + lastCore = val + } + ok, val, err = extractValue(line, nodeRegExp) + if err != nil { + return nil, -1, fmt.Errorf("could not parse node info from %q: %v", line, err) + } + if ok { + lastNode = val + } + } + nodeIdx, err := addNode(&nodes, lastNode) + if err != nil { + return nil, -1, fmt.Errorf("failed to add node %d: %v", lastNode, err) + } + nodes[nodeIdx].AddThread(lastThread, lastCore) + if numCores < 1 { + return nil, numCores, fmt.Errorf("could not detect any cores") + } + for idx, node := range nodes { + caches, err := sysinfo.GetCacheInfo(sysFs, node.Cores[0].Threads[0]) + if err != nil { + glog.Errorf("failed to get cache information for node %d: %v", node.Id, err) + continue + } + numThreadsPerCore := len(node.Cores[0].Threads) + numThreadsPerNode := len(node.Cores) * numThreadsPerCore + for _, cache := range caches { + c := info.Cache{ + Size: cache.Size, + Level: cache.Level, + Type: cache.Type, + } + if cache.Cpus == numThreadsPerNode && cache.Level > 2 { + // Add a node-level cache. + nodes[idx].AddNodeCache(c) + } else if cache.Cpus == numThreadsPerCore { + // Add to each core. + nodes[idx].AddPerCoreCache(c) + } + // Ignore unknown caches. + } + } + return nodes, numCores, nil +} + +func extractValue(s string, r *regexp.Regexp) (bool, int, error) { + matches := r.FindSubmatch([]byte(s)) + if len(matches) == 2 { + val, err := strconv.ParseInt(string(matches[1]), 10, 32) + if err != nil { + return true, -1, err + } + return true, int(val), nil + } + return false, -1, nil +} + +func findNode(nodes []info.Node, id int) (bool, int) { + for i, n := range nodes { + if n.Id == id { + return true, i + } + } + return false, -1 +} + +func addNode(nodes *[]info.Node, id int) (int, error) { + var idx int + if id == -1 { + // Some VMs don't fill topology data. Export single package. + id = 0 + } + + ok, idx := findNode(*nodes, id) + if !ok { + // New node + node := info.Node{Id: id} + // Add per-node memory information. + meminfo := fmt.Sprintf("/sys/devices/system/node/node%d/meminfo", id) + out, err := ioutil.ReadFile(meminfo) + // Ignore if per-node info is not available. + if err == nil { + m, err := parseCapacity(out, memoryCapacityRegexp) + if err != nil { + return -1, err + } + node.Memory = uint64(m) + } + *nodes = append(*nodes, node) + idx = len(*nodes) - 1 + } + return idx, nil +} diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/manager/testdata/cpuinfo b/Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/testdata/cpuinfo similarity index 100% rename from Godeps/_workspace/src/github.com/google/cadvisor/manager/testdata/cpuinfo rename to Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/testdata/cpuinfo diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/manager/topology_test.go b/Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/topology_test.go similarity index 94% rename from Godeps/_workspace/src/github.com/google/cadvisor/manager/topology_test.go rename to Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/topology_test.go index dbae6df3c83..0c3f158872d 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/manager/topology_test.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/utils/machine/topology_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package machine import ( "io/ioutil" @@ -38,7 +38,7 @@ func TestTopology(t *testing.T) { Cpus: 2, } sysFs.SetCacheInfo(c) - topology, numCores, err := getTopology(sysFs, string(testcpuinfo)) + topology, numCores, err := GetTopology(sysFs, string(testcpuinfo)) if err != nil { t.Errorf("failed to get topology for sample cpuinfo %s", string(testcpuinfo)) } @@ -84,7 +84,7 @@ func TestTopologyWithSimpleCpuinfo(t *testing.T) { Cpus: 1, } sysFs.SetCacheInfo(c) - topology, numCores, err := getTopology(sysFs, "processor\t: 0\n") + topology, numCores, err := GetTopology(sysFs, "processor\t: 0\n") if err != nil { t.Errorf("Expected cpuinfo with no topology data to succeed.") } @@ -110,7 +110,7 @@ func TestTopologyWithSimpleCpuinfo(t *testing.T) { } func TestTopologyEmptyCpuinfo(t *testing.T) { - _, _, err := getTopology(&fakesysfs.FakeSysFs{}, "") + _, _, err := GetTopology(&fakesysfs.FakeSysFs{}, "") if err == nil { t.Errorf("Expected empty cpuinfo to fail.") } diff --git a/Godeps/_workspace/src/github.com/google/cadvisor/version/version.go b/Godeps/_workspace/src/github.com/google/cadvisor/version/version.go index 5ea76cfcd30..97ea3e9157f 100644 --- a/Godeps/_workspace/src/github.com/google/cadvisor/version/version.go +++ b/Godeps/_workspace/src/github.com/google/cadvisor/version/version.go @@ -15,4 +15,4 @@ package version // Version of cAdvisor. -const VERSION = "0.15.1" +const VERSION = "0.16.0"