diff --git a/.gitignore b/.gitignore index c3ca3065..99537de4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ cadvisor # Go test binaries *.test + +# Files generated by JetBrains IDEs, e.g. IntelliJ IDEA +.idea/ +*.iml diff --git a/cadvisor.go b/cadvisor.go index 70faa2a2..df8146a4 100644 --- a/cadvisor.go +++ b/cadvisor.go @@ -33,6 +33,7 @@ import ( "github.com/google/cadvisor/version" "crypto/tls" + "github.com/golang/glog" ) @@ -60,13 +61,17 @@ var collectorKey = flag.String("collector_key", "", "Key for the collector's cer var ( // Metrics to be ignored. // Tcp metrics are ignored by default. - ignoreMetrics metricSetValue = metricSetValue{container.MetricSet{container.NetworkTcpUsageMetrics: struct{}{}}} + ignoreMetrics metricSetValue = metricSetValue{container.MetricSet{ + container.NetworkTcpUsageMetrics: struct{}{}, + container.NetworkUdpUsageMetrics: struct{}{}, + }} // List of metrics that can be ignored. ignoreWhitelist = container.MetricSet{ container.DiskUsageMetrics: struct{}{}, container.NetworkUsageMetrics: struct{}{}, container.NetworkTcpUsageMetrics: struct{}{}, + container.NetworkUdpUsageMetrics: struct{}{}, } ) @@ -98,7 +103,7 @@ func (ml *metricSetValue) Set(value string) error { } func init() { - flag.Var(&ignoreMetrics, "disable_metrics", "comma-separated list of `metrics` to be disabled. Options are 'disk', 'network', 'tcp'. Note: tcp is disabled by default due to high CPU usage.") + flag.Var(&ignoreMetrics, "disable_metrics", "comma-separated list of `metrics` to be disabled. Options are 'disk', 'network', 'tcp', 'udp'. Note: tcp and udp are disabled by default due to high CPU usage.") } func main() { diff --git a/cadvisor_test.go b/cadvisor_test.go index 4c326f14..59bbf779 100644 --- a/cadvisor_test.go +++ b/cadvisor_test.go @@ -28,6 +28,12 @@ func TestTcpMetricsAreDisabledByDefault(t *testing.T) { assert.True(t, ignoreMetrics.Has(container.NetworkTcpUsageMetrics)) } +func TestUdpMetricsAreDisabledByDefault(t *testing.T) { + assert.True(t, ignoreMetrics.Has(container.NetworkUdpUsageMetrics)) + flag.Parse() + assert.True(t, ignoreMetrics.Has(container.NetworkUdpUsageMetrics)) +} + func TestIgnoreMetrics(t *testing.T) { tests := []struct { value string diff --git a/client/client.go b/client/client.go index 325f120d..220cb124 100644 --- a/client/client.go +++ b/client/client.go @@ -31,21 +31,35 @@ import ( "github.com/google/cadvisor/info/v1" "github.com/golang/glog" + "time" ) // Client represents the base URL for a cAdvisor client. type Client struct { - baseUrl string + baseUrl string + httpClient *http.Client } // NewClient returns a new v1.3 client with the specified base URL. func NewClient(url string) (*Client, error) { + return newClient(url, http.DefaultClient) +} + +// NewClientWithTimeout returns a new v1.3 client with the specified base URL and http client timeout. +func NewClientWithTimeout(url string, timeout time.Duration) (*Client, error) { + return newClient(url, &http.Client{ + Timeout: timeout, + }) +} + +func newClient(url string, client *http.Client) (*Client, error) { if !strings.HasSuffix(url, "/") { url += "/" } return &Client{ - baseUrl: fmt.Sprintf("%sapi/v1.3/", url), + baseUrl: fmt.Sprintf("%sapi/v1.3/", url), + httpClient: client, }, nil } @@ -168,9 +182,9 @@ func (self *Client) httpGetJsonData(data, postData interface{}, url, infoName st if marshalErr != nil { return fmt.Errorf("unable to marshal data: %v", marshalErr) } - resp, err = http.Post(url, "application/json", bytes.NewBuffer(data)) + resp, err = self.httpClient.Post(url, "application/json", bytes.NewBuffer(data)) } else { - resp, err = http.Get(url) + resp, err = self.httpClient.Get(url) } if err != nil { return fmt.Errorf("unable to get %q from %q: %v", infoName, url, err) @@ -199,7 +213,7 @@ func (self *Client) getEventStreamingData(url string, einfo chan *v1.Event) erro if err != nil { return err } - resp, err := http.DefaultClient.Do(req) + resp, err := self.httpClient.Do(req) if err != nil { return err } diff --git a/container/docker/docker.go b/container/docker/docker.go index ae9f1fdf..14c717a7 100644 --- a/container/docker/docker.go +++ b/container/docker/docker.go @@ -17,6 +17,7 @@ package docker import ( "fmt" + "regexp" "strconv" "strings" @@ -39,6 +40,7 @@ func Status() (v1.DockerStatus, error) { out := v1.DockerStatus{} out.Version = VersionString() + out.APIVersion = APIVersionString() out.KernelVersion = machine.KernelVersion() out.OS = dockerInfo.OperatingSystem out.Hostname = dockerInfo.Name @@ -105,7 +107,7 @@ func ValidateInfo() (*dockertypes.Info, error) { } dockerInfo.ServerVersion = version.Version } - version, err := parseDockerVersion(dockerInfo.ServerVersion) + version, err := parseVersion(dockerInfo.ServerVersion, version_re, 3) if err != nil { return nil, err } @@ -129,7 +131,11 @@ func ValidateInfo() (*dockertypes.Info, error) { } func Version() ([]int, error) { - return parseDockerVersion(VersionString()) + return parseVersion(VersionString(), version_re, 3) +} + +func APIVersion() ([]int, error) { + return parseVersion(APIVersionString(), apiversion_re, 2) } func VersionString() string { @@ -144,18 +150,29 @@ func VersionString() string { return docker_version } -// TODO: switch to a semantic versioning library. -func parseDockerVersion(full_version_string string) ([]int, error) { - matches := version_re.FindAllStringSubmatch(full_version_string, -1) +func APIVersionString() string { + docker_api_version := "Unknown" + client, err := Client() + if err == nil { + version, err := client.ServerVersion(context.Background()) + if err == nil { + docker_api_version = version.APIVersion + } + } + return docker_api_version +} + +func parseVersion(version_string string, regex *regexp.Regexp, length int) ([]int, error) { + matches := regex.FindAllStringSubmatch(version_string, -1) if len(matches) != 1 { - return nil, fmt.Errorf("version string \"%v\" doesn't match expected regular expression: \"%v\"", full_version_string, version_regexp_string) + return nil, fmt.Errorf("version string \"%v\" doesn't match expected regular expression: \"%v\"", version_string, regex.String()) } version_string_array := matches[0][1:] - version_array := make([]int, 3) - for index, version_string := range version_string_array { - version, err := strconv.Atoi(version_string) + version_array := make([]int, length) + for index, version_str := range version_string_array { + version, err := strconv.Atoi(version_str) if err != nil { - return nil, fmt.Errorf("error while parsing \"%v\" in \"%v\"", version_string, full_version_string) + return nil, fmt.Errorf("error while parsing \"%v\" in \"%v\"", version_str, version_string) } version_array[index] = version } diff --git a/container/docker/docker_test.go b/container/docker/docker_test.go new file mode 100644 index 00000000..07bb784d --- /dev/null +++ b/container/docker/docker_test.go @@ -0,0 +1,51 @@ +// Copyright 2017 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 docker + +import ( + "reflect" + "regexp" + "testing" +) + +func TestParseDockerAPIVersion(t *testing.T) { + tests := []struct { + version string + regex *regexp.Regexp + length int + expected []int + expectedError string + }{ + {"17.03.0", version_re, 3, []int{17, 03, 0}, ""}, + {"17.a3.0", version_re, 3, []int{}, `version string "17.a3.0" doesn't match expected regular expression: "(\d+)\.(\d+)\.(\d+)"`}, + {"1.20", apiversion_re, 2, []int{1, 20}, ""}, + {"1.a", apiversion_re, 2, []int{}, `version string "1.a" doesn't match expected regular expression: "(\d+)\.(\d+)"`}, + } + + for _, test := range tests { + actual, err := parseVersion(test.version, test.regex, test.length) + if err != nil { + if len(test.expectedError) == 0 { + t.Errorf("%s: expected no error, got %v", test.version, err) + } else if err.Error() != test.expectedError { + t.Errorf("%s: expected error %v, got %v", test.version, test.expectedError, err) + } + } else { + if !reflect.DeepEqual(actual, test.expected) { + t.Errorf("%s: expected array %v, got %v", test.version, test.expected, actual) + } + } + } +} diff --git a/container/docker/factory.go b/container/docker/factory.go index 08beeddf..09d1d3aa 100644 --- a/container/docker/factory.go +++ b/container/docker/factory.go @@ -103,6 +103,8 @@ type dockerFactory struct { dockerVersion []int + dockerAPIVersion []int + ignoreMetrics container.MetricSet thinPoolWatcher *devicemapper.ThinPoolWatcher @@ -185,8 +187,10 @@ func (self *dockerFactory) DebugInfo() map[string][]string { } var ( - version_regexp_string = `(\d+)\.(\d+)\.(\d+)` - version_re = regexp.MustCompile(version_regexp_string) + version_regexp_string = `(\d+)\.(\d+)\.(\d+)` + version_re = regexp.MustCompile(version_regexp_string) + apiversion_regexp_string = `(\d+)\.(\d+)` + apiversion_re = regexp.MustCompile(apiversion_regexp_string) ) func startThinPoolWatcher(dockerInfo *dockertypes.Info) (*devicemapper.ThinPoolWatcher, error) { @@ -310,7 +314,9 @@ func Register(factory info.MachineInfoFactory, fsInfo fs.FsInfo, ignoreMetrics c } // Version already validated above, assume no error here. - dockerVersion, _ := parseDockerVersion(dockerInfo.ServerVersion) + dockerVersion, _ := parseVersion(dockerInfo.ServerVersion, version_re, 3) + + dockerAPIVersion, _ := APIVersion() cgroupSubsystems, err := libcontainer.GetCgroupSubsystems() if err != nil { @@ -338,6 +344,7 @@ func Register(factory info.MachineInfoFactory, fsInfo fs.FsInfo, ignoreMetrics c cgroupSubsystems: cgroupSubsystems, client: client, dockerVersion: dockerVersion, + dockerAPIVersion: dockerAPIVersion, fsInfo: fsInfo, machineInfoFactory: factory, storageDriver: storageDriver(dockerInfo.Driver), diff --git a/container/factory.go b/container/factory.go index 5d0e0007..befb4a9e 100644 --- a/container/factory.go +++ b/container/factory.go @@ -48,6 +48,7 @@ const ( DiskUsageMetrics MetricKind = "disk" NetworkUsageMetrics MetricKind = "network" NetworkTcpUsageMetrics MetricKind = "tcp" + NetworkUdpUsageMetrics MetricKind = "udp" AppMetrics MetricKind = "app" ) diff --git a/container/libcontainer/helpers.go b/container/libcontainer/helpers.go index 69116a35..1c382249 100644 --- a/container/libcontainer/helpers.go +++ b/container/libcontainer/helpers.go @@ -17,6 +17,7 @@ package libcontainer import ( "bufio" "fmt" + "io" "io/ioutil" "os" "path" @@ -118,6 +119,21 @@ func GetStats(cgroupManager cgroups.Manager, rootFs string, pid int, ignoreMetri stats.Network.Tcp6 = t6 } } + if !ignoreMetrics.Has(container.NetworkUdpUsageMetrics) { + u, err := udpStatsFromProc(rootFs, pid, "net/udp") + if err != nil { + glog.V(2).Infof("Unable to get udp stats from pid %d: %v", pid, err) + } else { + stats.Network.Udp = u + } + + u6, err := udpStatsFromProc(rootFs, pid, "net/udp6") + if err != nil { + glog.V(2).Infof("Unable to get udp6 stats from pid %d: %v", pid, err) + } else { + stats.Network.Udp6 = u6 + } + } // For backwards compatibility. if len(stats.Network.Interfaces) > 0 { @@ -291,6 +307,74 @@ func scanTcpStats(tcpStatsFile string) (info.TcpStat, error) { return stats, nil } +func udpStatsFromProc(rootFs string, pid int, file string) (info.UdpStat, error) { + var err error + var udpStats info.UdpStat + + udpStatsFile := path.Join(rootFs, "proc", strconv.Itoa(pid), file) + + r, err := os.Open(udpStatsFile) + if err != nil { + return udpStats, fmt.Errorf("failure opening %s: %v", udpStatsFile, err) + } + + udpStats, err = scanUdpStats(r) + if err != nil { + return udpStats, fmt.Errorf("couldn't read udp stats: %v", err) + } + + return udpStats, nil +} + +func scanUdpStats(r io.Reader) (info.UdpStat, error) { + var stats info.UdpStat + + scanner := bufio.NewScanner(r) + scanner.Split(bufio.ScanLines) + + // Discard header line + if b := scanner.Scan(); !b { + return stats, scanner.Err() + } + + listening := uint64(0) + dropped := uint64(0) + rxQueued := uint64(0) + txQueued := uint64(0) + + for scanner.Scan() { + line := scanner.Text() + // Format: sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops + + listening++ + + fs := strings.Fields(line) + if len(fs) != 13 { + continue + } + + rx, tx := uint64(0), uint64(0) + fmt.Sscanf(fs[4], "%X:%X", &rx, &tx) + rxQueued += rx + txQueued += tx + + d, err := strconv.Atoi(string(fs[12])) + if err != nil { + continue + } + dropped += uint64(d) + } + + stats = info.UdpStat{ + Listen: listening, + Dropped: dropped, + RxQueued: rxQueued, + TxQueued: txQueued, + } + + return stats, nil +} + func GetProcesses(cgroupManager cgroups.Manager) ([]int, error) { pids, err := cgroupManager.GetPids() if err != nil { diff --git a/container/libcontainer/helpers_test.go b/container/libcontainer/helpers_test.go index f0a637c1..c491e287 100644 --- a/container/libcontainer/helpers_test.go +++ b/container/libcontainer/helpers_test.go @@ -15,6 +15,7 @@ package libcontainer import ( + "os" "testing" info "github.com/google/cadvisor/info/v1" @@ -61,3 +62,27 @@ func TestScanInterfaceStats(t *testing.T) { } } } + +func TestScanUDPStats(t *testing.T) { + udpStatsFile := "testdata/procnetudp" + r, err := os.Open(udpStatsFile) + if err != nil { + t.Errorf("failure opening %s: %v", udpStatsFile, err) + } + + stats, err := scanUdpStats(r) + if err != nil { + t.Error(err) + } + + var udpstats = info.UdpStat{ + Listen: 2, + Dropped: 4, + RxQueued: 10, + TxQueued: 11, + } + + if stats != udpstats { + t.Errorf("Expected %#v, got %#v", udpstats, stats) + } +} diff --git a/container/libcontainer/testdata/procnetudp b/container/libcontainer/testdata/procnetudp new file mode 100644 index 00000000..de1718c1 --- /dev/null +++ b/container/libcontainer/testdata/procnetudp @@ -0,0 +1,3 @@ + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops + 1: 00000000:07D3 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 16583 2 ffff8800b4549400 0 + 21: 00000000:A841 00000000:0000 07 0000000A:0000000B 00:00000000 00000000 1000 0 114299623 2 ffff880338477800 4 diff --git a/info/v1/container.go b/info/v1/container.go index 6c9be7c6..6127d881 100644 --- a/info/v1/container.go +++ b/info/v1/container.go @@ -386,6 +386,10 @@ type NetworkStats struct { Tcp TcpStat `json:"tcp"` // TCP6 connection stats (Established, Listen...) Tcp6 TcpStat `json:"tcp6"` + // UDP connection stats + Udp UdpStat `json:"udp"` + // UDP6 connection stats + Udp6 UdpStat `json:"udp6"` } type TcpStat struct { @@ -413,6 +417,20 @@ type TcpStat struct { Closing uint64 } +type UdpStat struct { + // Count of UDP sockets in state "Listen" + Listen uint64 + + // Count of UDP packets dropped by the IP stack + Dropped uint64 + + // Count of packets Queued for Receieve + RxQueued uint64 + + // Count of packets Queued for Transmit + TxQueued uint64 +} + type FsStats struct { // The block device name associated with the filesystem. Device string `json:"device,omitempty"` diff --git a/info/v1/docker.go b/info/v1/docker.go index 2703c534..7b5fb713 100644 --- a/info/v1/docker.go +++ b/info/v1/docker.go @@ -17,6 +17,7 @@ package v1 type DockerStatus struct { Version string `json:"version"` + APIVersion string `json:"api_version"` KernelVersion string `json:"kernel_version"` OS string `json:"os"` Hostname string `json:"hostname"` diff --git a/info/v1/machine.go b/info/v1/machine.go index 933b6f30..c259e0ba 100644 --- a/info/v1/machine.go +++ b/info/v1/machine.go @@ -196,6 +196,9 @@ type VersionInfo struct { // Docker version. DockerVersion string `json:"docker_version"` + // Docker API Version + DockerAPIVersion string `json:"docker_api_version"` + // cAdvisor version. CadvisorVersion string `json:"cadvisor_version"` // cAdvisor git revision. diff --git a/info/v2/container.go b/info/v2/container.go index f3f67a55..cda1208a 100644 --- a/info/v2/container.go +++ b/info/v2/container.go @@ -269,6 +269,10 @@ type NetworkStats struct { Tcp TcpStat `json:"tcp"` // TCP6 connection stats (Established, Listen...) Tcp6 TcpStat `json:"tcp6"` + // UDP connection stats + Udp v1.UdpStat `json:"udp"` + // UDP6 connection stats + Udp6 v1.UdpStat `json:"udp6"` } // Instantaneous CPU stats diff --git a/info/v2/conversion.go b/info/v2/conversion.go index c099b904..1c0f91f8 100644 --- a/info/v2/conversion.go +++ b/info/v2/conversion.go @@ -133,7 +133,7 @@ func ContainerStatsFromV1(containerName string, spec *v1.ContainerSpec, stats [] } } else if len(val.Filesystem) > 1 && containerName != "/" { // Cannot handle multiple devices per container. - glog.V(2).Infof("failed to handle multiple devices for container %s. Skipping Filesystem stats", containerName) + glog.V(4).Infof("failed to handle multiple devices for container %s. Skipping Filesystem stats", containerName) } } if spec.HasDiskIo { @@ -259,6 +259,7 @@ func ContainerSpecFromV1(specV1 *v1.ContainerSpec, aliases []string, namespace s HasCustomMetrics: specV1.HasCustomMetrics, Image: specV1.Image, Labels: specV1.Labels, + Envs: specV1.Envs, } if specV1.HasCpu { specV2.Cpu.Limit = specV1.Cpu.Limit diff --git a/info/v2/conversion_test.go b/info/v2/conversion_test.go index be9cc92c..445fcb09 100644 --- a/info/v2/conversion_test.go +++ b/info/v2/conversion_test.go @@ -26,12 +26,14 @@ import ( var ( timestamp = time.Date(1987, time.August, 10, 0, 0, 0, 0, time.UTC) labels = map[string]string{"foo": "bar"} + envs = map[string]string{"foo": "bar"} ) func TestContanierSpecFromV1(t *testing.T) { v1Spec := v1.ContainerSpec{ CreationTime: timestamp, Labels: labels, + Envs: envs, HasCpu: true, Cpu: v1.CpuSpec{ Limit: 2048, @@ -63,6 +65,7 @@ func TestContanierSpecFromV1(t *testing.T) { expectedV2Spec := ContainerSpec{ CreationTime: timestamp, Labels: labels, + Envs: envs, HasCpu: true, Cpu: CpuSpec{ Limit: 2048, diff --git a/info/v2/machine.go b/info/v2/machine.go index 0e5a6136..ecf04bf5 100644 --- a/info/v2/machine.go +++ b/info/v2/machine.go @@ -31,6 +31,9 @@ type Attributes struct { // Docker version. DockerVersion string `json:"docker_version"` + // Docker API version. + DockerAPIVersion string `json:"docker_api_version"` + // cAdvisor version. CadvisorVersion string `json:"cadvisor_version"` @@ -74,6 +77,7 @@ func GetAttributes(mi *v1.MachineInfo, vi *v1.VersionInfo) Attributes { KernelVersion: vi.KernelVersion, ContainerOsVersion: vi.ContainerOsVersion, DockerVersion: vi.DockerVersion, + DockerAPIVersion: vi.DockerAPIVersion, CadvisorVersion: vi.CadvisorVersion, NumCores: mi.NumCores, CpuFrequency: mi.CpuFrequency, diff --git a/manager/manager.go b/manager/manager.go index 4de5e650..dc5273ac 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -1255,11 +1255,13 @@ func getVersionInfo() (*info.VersionInfo, error) { kernel_version := machine.KernelVersion() container_os := machine.ContainerOsVersion() docker_version := docker.VersionString() + docker_api_version := docker.APIVersionString() return &info.VersionInfo{ KernelVersion: kernel_version, ContainerOsVersion: container_os, DockerVersion: docker_version, + DockerAPIVersion: docker_api_version, CadvisorVersion: version.Info["version"], CadvisorRevision: version.Info["revision"], }, nil diff --git a/metrics/prometheus.go b/metrics/prometheus.go index 775866bb..6f65cb45 100644 --- a/metrics/prometheus.go +++ b/metrics/prometheus.go @@ -130,10 +130,12 @@ func NewPrometheusCollector(i infoProvider, f ContainerLabelsFunc) *PrometheusCo getValues: func(s *info.ContainerStats) metricValues { values := make(metricValues, 0, len(s.Cpu.Usage.PerCpu)) for i, value := range s.Cpu.Usage.PerCpu { - values = append(values, metricValue{ - value: float64(value) / float64(time.Second), - labels: []string{fmt.Sprintf("cpu%02d", i)}, - }) + if value > 0 { + values = append(values, metricValue{ + value: float64(value) / float64(time.Second), + labels: []string{fmt.Sprintf("cpu%02d", i)}, + }) + } } return values }, diff --git a/pages/docker.go b/pages/docker.go index 8cc0177c..3d40e2f6 100644 --- a/pages/docker.go +++ b/pages/docker.go @@ -40,6 +40,7 @@ func toStatusKV(status info.DockerStatus) ([]keyVal, []keyVal) { } return []keyVal{ {Key: "Docker Version", Value: status.Version}, + {Key: "Docker API Version", Value: status.APIVersion}, {Key: "Kernel Version", Value: status.KernelVersion}, {Key: "OS Version", Value: status.OS}, {Key: "Host Name", Value: status.Hostname},