From 4c67b21c1d5e7e80992583a8983505a26123e930 Mon Sep 17 00:00:00 2001 From: anushree-n Date: Thu, 13 Aug 2015 09:51:32 -0700 Subject: [PATCH] Add Prometheus Collector --- collector/config.go | 11 ++ .../config/sample_config_prometheus.json | 6 + collector/prometheus_collector.go | 166 ++++++++++++++++++ collector/prometheus_collector_test.go | 64 +++++++ manager/manager.go | 31 +++- 5 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 collector/config/sample_config_prometheus.json create mode 100644 collector/prometheus_collector.go create mode 100644 collector/prometheus_collector_test.go diff --git a/collector/config.go b/collector/config.go index fc120703..8a3234de 100644 --- a/collector/config.go +++ b/collector/config.go @@ -48,3 +48,14 @@ type MetricConfig struct { //the regular expression that can be used to extract the metric Regex string `json:"regex"` } + +type Prometheus struct { + //the endpoint to hit to scrape metrics + Endpoint string `json:"endpoint"` + + //the frequency at which metrics should be collected + PollingFrequency time.Duration `json:"polling_frequency"` + + //holds names of different metrics that can be collected + MetricsConfig []string `json:"metrics_config"` +} diff --git a/collector/config/sample_config_prometheus.json b/collector/config/sample_config_prometheus.json new file mode 100644 index 00000000..cc74bb72 --- /dev/null +++ b/collector/config/sample_config_prometheus.json @@ -0,0 +1,6 @@ +{ + "endpoint" : "http://localhost:8080/metrics", + "polling_frequency" : 10, + "metrics_config" : [ + ] +} diff --git a/collector/prometheus_collector.go b/collector/prometheus_collector.go new file mode 100644 index 00000000..ca332d59 --- /dev/null +++ b/collector/prometheus_collector.go @@ -0,0 +1,166 @@ +// 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" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + + "github.com/google/cadvisor/info/v1" +) + +type PrometheusCollector struct { + //name of the collector + name string + + //rate at which metrics are collected + pollingFrequency time.Duration + + //holds information extracted from the config file for a collector + configFile Prometheus +} + +//Returns a new collector using the information extracted from the configfile +func NewPrometheusCollector(collectorName string, configFile []byte) (*PrometheusCollector, error) { + var configInJSON Prometheus + err := json.Unmarshal(configFile, &configInJSON) + if err != nil { + return nil, err + } + + minPollingFrequency := configInJSON.PollingFrequency + + // Minimum supported frequency is 1s + minSupportedFrequency := 1 * time.Second + + if minPollingFrequency < minSupportedFrequency { + minPollingFrequency = minSupportedFrequency + } + + //TODO : Add checks for validity of config file (eg : Accurate JSON fields) + return &PrometheusCollector{ + name: collectorName, + pollingFrequency: minPollingFrequency, + configFile: configInJSON, + }, nil +} + +//Returns name of the collector +func (collector *PrometheusCollector) Name() string { + return collector.name +} + +func getMetricData(line string) string { + fields := strings.Fields(line) + data := fields[3] + if len(fields) > 4 { + for i := range fields { + if i > 3 { + data = data + "_" + fields[i] + } + } + } + return strings.TrimSpace(data) +} + +func (collector *PrometheusCollector) GetSpec() []v1.MetricSpec { + specs := []v1.MetricSpec{} + response, err := http.Get(collector.configFile.Endpoint) + if err != nil { + return specs + } + defer response.Body.Close() + + pageContent, err := ioutil.ReadAll(response.Body) + if err != nil { + return specs + } + + lines := strings.Split(string(pageContent), "\n") + for i, line := range lines { + if strings.HasPrefix(line, "# HELP") { + stopIndex := strings.Index(lines[i+2], "{") + if stopIndex == -1 { + stopIndex = strings.Index(lines[i+2], " ") + } + spec := v1.MetricSpec{ + Name: strings.TrimSpace(lines[i+2][0:stopIndex]), + Type: v1.MetricType(getMetricData(lines[i+1])), + Format: "float", + Units: getMetricData(lines[i]), + } + specs = append(specs, spec) + } + } + return specs +} + +//Returns collected metrics and the next collection time of the collector +func (collector *PrometheusCollector) Collect(metrics map[string][]v1.MetricVal) (time.Time, map[string][]v1.MetricVal, error) { + currentTime := time.Now() + nextCollectionTime := currentTime.Add(time.Duration(collector.pollingFrequency)) + + 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 + lines := strings.Split(string(pageContent), "\n") + + for _, line := range lines { + if line == "" { + break + } + if !strings.HasPrefix(line, "# HELP") && !strings.HasPrefix(line, "# TYPE") { + var metLabel string + startLabelIndex := strings.Index(line, "{") + spaceIndex := strings.Index(line, " ") + if startLabelIndex == -1 { + startLabelIndex = spaceIndex + } + + metName := strings.TrimSpace(line[0:startLabelIndex]) + + if startLabelIndex+1 <= spaceIndex-1 { + metLabel = strings.TrimSpace(line[(startLabelIndex + 1):(spaceIndex - 1)]) + } + + metVal, err := strconv.ParseFloat(line[spaceIndex+1:], 64) + if err != nil { + errorSlice = append(errorSlice, err) + } + + metric := v1.MetricVal{ + Label: metLabel, + FloatValue: metVal, + Timestamp: currentTime, + } + metrics[metName] = append(metrics[metName], metric) + } + } + return nextCollectionTime, metrics, compileErrors(errorSlice) +} diff --git a/collector/prometheus_collector_test.go b/collector/prometheus_collector_test.go new file mode 100644 index 00000000..0ead785d --- /dev/null +++ b/collector/prometheus_collector_test.go @@ -0,0 +1,64 @@ +// 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" + "testing" + + "github.com/google/cadvisor/info/v1" + "github.com/stretchr/testify/assert" +) + +func TestPrometheus(t *testing.T) { + assert := assert.New(t) + + //Create a prometheus collector using the config file 'sample_config_prometheus.json' + configFile, err := ioutil.ReadFile("config/sample_config_prometheus.json") + collector, err := NewPrometheusCollector("Prometheus", configFile) + assert.NoError(err) + assert.Equal(collector.name, "Prometheus") + assert.Equal(collector.configFile.Endpoint, "http://localhost:8080/metrics") + + tempServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + text := "# HELP go_gc_duration_seconds A summary of the GC invocation durations.\n" + text += "# TYPE go_gc_duration_seconds summary\n" + text += "go_gc_duration_seconds{quantile=\"0\"} 5.8348000000000004e-05\n" + text += "go_gc_duration_seconds{quantile=\"1\"} 0.000499764\n" + text += "# HELP go_goroutines Number of goroutines that currently exist.\n" + text += "# TYPE go_goroutines gauge\n" + text += "go_goroutines 16" + fmt.Fprintln(w, text) + })) + + defer tempServer.Close() + + collector.configFile.Endpoint = tempServer.URL + metrics := map[string][]v1.MetricVal{} + _, metrics, errMetric := collector.Collect(metrics) + + assert.NoError(errMetric) + + go_gc_duration := metrics["go_gc_duration_seconds"] + assert.Equal(go_gc_duration[0].FloatValue, 5.8348000000000004e-05) + assert.Equal(go_gc_duration[1].FloatValue, 0.000499764) + + goRoutines := metrics["go_goroutines"] + assert.Equal(goRoutines[0].FloatValue, 16) +} diff --git a/manager/manager.go b/manager/manager.go index 751acb97..ee6ca452 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -705,15 +705,28 @@ func (m *manager) registerCollectors(collectorConfigs map[string]string, cont *c } 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 + if strings.HasPrefix(k, "prometheus") || strings.HasPrefix(k, "Prometheus") { + newCollector, err := collector.NewPrometheusCollector(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 + } + } else { + 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