Quellcode durchsuchen

Add kubecost_cluster_info metric emission, cleanup ClusterInfo path a bit.

Matt Bolt vor 5 Jahren
Ursprung
Commit
396d08858c
5 geänderte Dateien mit 249 neuen und 43 gelöschten Zeilen
  1. 70 0
      pkg/costmodel/clusterinfo.go
  2. 76 0
      pkg/costmodel/metrics.go
  3. 6 43
      pkg/costmodel/router.go
  4. 69 0
      pkg/prom/metrics.go
  5. 28 0
      test/clusterinfo_test.go

+ 70 - 0
pkg/costmodel/clusterinfo.go

@@ -0,0 +1,70 @@
+package costmodel
+
+import (
+	"fmt"
+
+	cloudProvider "github.com/kubecost/cost-model/pkg/cloud"
+	"github.com/kubecost/cost-model/pkg/env"
+	"github.com/kubecost/cost-model/pkg/thanos"
+
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/klog"
+)
+
+var (
+	logCollectionEnabled    bool   = env.IsLogCollectionEnabled()
+	productAnalyticsEnabled bool   = env.IsProductAnalyticsEnabled()
+	errorReportingEnabled   bool   = env.IsErrorReportingEnabled()
+	valuesReportingEnabled  bool   = env.IsValuesReportingEnabled()
+	clusterProfile          string = env.GetClusterProfile()
+)
+
+// writeReportingFlags writes the reporting flags to the cluster info map
+func writeReportingFlags(clusterInfo map[string]string) {
+	clusterInfo["logCollection"] = fmt.Sprintf("%t", logCollectionEnabled)
+	clusterInfo["productAnalytics"] = fmt.Sprintf("%t", productAnalyticsEnabled)
+	clusterInfo["errorReporting"] = fmt.Sprintf("%t", errorReportingEnabled)
+	clusterInfo["valuesReporting"] = fmt.Sprintf("%t", valuesReportingEnabled)
+}
+
+// writeClusterProfile writes the data associated with the cluster profile
+func writeClusterProfile(clusterInfo map[string]string) {
+	clusterInfo["clusterProfile"] = clusterProfile
+}
+
+func writeThanosFlags(clusterInfo map[string]string) {
+	// Include Thanos Offset Duration if Applicable
+	clusterInfo["thanosEnabled"] = fmt.Sprintf("%t", thanos.IsEnabled())
+	if thanos.IsEnabled() {
+		clusterInfo["thanosOffset"] = thanos.Offset()
+	}
+}
+
+// GetClusterInfo provides specific information about the cluster cloud provider as well as
+// generic configuration values.
+func GetClusterInfo(kubeClient kubernetes.Interface, cloud cloudProvider.Provider) map[string]string {
+	data, err := cloud.ClusterInfo()
+
+	// Ensure we create the info object if it doesn't exist
+	if data == nil {
+		data = make(map[string]string)
+	}
+
+	kc, ok := kubeClient.(*kubernetes.Clientset)
+	if ok && data != nil {
+		v, err := kc.ServerVersion()
+		if err != nil {
+			klog.Infof("Could not get k8s version info: %s", err.Error())
+		} else if v != nil {
+			data["version"] = v.Major + "." + v.Minor
+		}
+	} else {
+		klog.Infof("Could not get k8s version info: %s", err.Error())
+	}
+
+	writeClusterProfile(data)
+	writeReportingFlags(data)
+	writeThanosFlags(data)
+
+	return data
+}

+ 76 - 0
pkg/costmodel/metrics.go

@@ -12,6 +12,7 @@ import (
 	costAnalyzerCloud "github.com/kubecost/cost-model/pkg/cloud"
 	"github.com/kubecost/cost-model/pkg/errors"
 	"github.com/kubecost/cost-model/pkg/log"
+	"github.com/kubecost/cost-model/pkg/prom"
 	"github.com/prometheus/client_golang/prometheus"
 	dto "github.com/prometheus/client_model/go"
 	v1 "k8s.io/api/core/v1"
@@ -283,6 +284,81 @@ func (s ServiceMetric) Write(m *dto.Metric) error {
 	return nil
 }
 
+//--------------------------------------------------------------------------
+//  ClusterInfoCollector
+//--------------------------------------------------------------------------
+
+// ClusterInfoCollector is a prometheus collector that generates ClusterInfoMetrics
+type ClusterInfoCollector struct {
+	Cloud         costAnalyzerCloud.Provider
+	KubeClientSet kubernetes.Interface
+}
+
+// Describe sends the super-set of all possible descriptors of metrics
+// collected by this Collector.
+func (cic ClusterInfoCollector) Describe(ch chan<- *prometheus.Desc) {
+	ch <- prometheus.NewDesc("kubecost_cluster_info", "Kubecost Cluster Info", []string{}, nil)
+}
+
+// Collect is called by the Prometheus registry when collecting metrics.
+func (cic ClusterInfoCollector) Collect(ch chan<- prometheus.Metric) {
+	clusterInfo := GetClusterInfo(cic.KubeClientSet, cic.Cloud)
+	labels := prom.MapToLabels(clusterInfo)
+
+	m := newClusterInfoMetric("kubecost_cluster_info", labels)
+	ch <- m
+}
+
+//--------------------------------------------------------------------------
+//  ClusterInfoMetric
+//--------------------------------------------------------------------------
+
+// ClusterInfoMetric is a prometheus.Metric used to encode the local cluster info
+type ClusterInfoMetric struct {
+	fqName string
+	help   string
+	labels map[string]string
+}
+
+// Creates a new ClusterInfoMetric, implementation of prometheus.Metric
+func newClusterInfoMetric(fqName string, labels map[string]string) ClusterInfoMetric {
+	return ClusterInfoMetric{
+		fqName: fqName,
+		labels: labels,
+		help:   "kubecost_cluster_info ClusterInfo",
+	}
+}
+
+// Desc returns the descriptor for the Metric. This method idempotently
+// returns the same descriptor throughout the lifetime of the Metric.
+func (cim ClusterInfoMetric) Desc() *prometheus.Desc {
+	l := prometheus.Labels{}
+	return prometheus.NewDesc(cim.fqName, cim.help, prom.LabelNamesFrom(cim.labels), l)
+}
+
+// Write encodes the Metric into a "Metric" Protocol Buffer data
+// transmission object.
+func (cim ClusterInfoMetric) Write(m *dto.Metric) error {
+	h := float64(1)
+	m.Gauge = &dto.Gauge{
+		Value: &h,
+	}
+	var labels []*dto.LabelPair
+	for k, v := range cim.labels {
+		labels = append(labels, &dto.LabelPair{
+			Name:  toStringPtr(k),
+			Value: toStringPtr(v),
+		})
+	}
+	m.Label = labels
+	return nil
+}
+
+// toStringPtr is used to create a new string pointer from iteration vars
+func toStringPtr(s string) *string {
+	return &s
+}
+
 //--------------------------------------------------------------------------
 //  Package Functions
 //--------------------------------------------------------------------------

+ 6 - 43
pkg/costmodel/router.go

@@ -45,11 +45,6 @@ const (
 var (
 	// gitCommit is set by the build system
 	gitCommit                       string
-	logCollectionEnabled            bool   = env.IsLogCollectionEnabled()
-	productAnalyticsEnabled         bool   = env.IsProductAnalyticsEnabled()
-	errorReportingEnabled           bool   = env.IsErrorReportingEnabled()
-	valuesReportingEnabled          bool   = env.IsValuesReportingEnabled()
-	clusterProfile                  string = env.GetClusterProfile()
 	multiclusterDBBasicAuthUsername string = env.GetMultiClusterBasicAuthUsername()
 	multiclusterDBBasicAuthPW       string = env.GetMultiClusterBasicAuthPassword()
 )
@@ -169,14 +164,6 @@ func normalizeTimeParam(param string) (string, error) {
 	return param, nil
 }
 
-// writeReportingFlags writes the reporting flags to the cluster info map
-func writeReportingFlags(clusterInfo map[string]string) {
-	clusterInfo["logCollection"] = fmt.Sprintf("%t", logCollectionEnabled)
-	clusterInfo["productAnalytics"] = fmt.Sprintf("%t", productAnalyticsEnabled)
-	clusterInfo["errorReporting"] = fmt.Sprintf("%t", errorReportingEnabled)
-	clusterInfo["valuesReporting"] = fmt.Sprintf("%t", valuesReportingEnabled)
-}
-
 // parsePercentString takes a string of expected format "N%" and returns a floating point 0.0N.
 // If the "%" symbol is missing, it just returns 0.0N. Empty string is interpreted as "0%" and
 // return 0.0.
@@ -613,37 +600,9 @@ func (p *Accesses) ClusterInfo(w http.ResponseWriter, r *http.Request, ps httpro
 	w.Header().Set("Content-Type", "application/json")
 	w.Header().Set("Access-Control-Allow-Origin", "*")
 
-	data, err := p.Cloud.ClusterInfo()
-
-	kc, ok := p.KubeClientSet.(*kubernetes.Clientset)
-	if ok && data != nil {
-		v, err := kc.ServerVersion()
-		if err != nil {
-			klog.Infof("Could not get k8s version info: %s", err.Error())
-		} else if v != nil {
-			data["version"] = v.Major + "." + v.Minor
-		}
-	} else {
-		klog.Infof("Could not get k8s version info: %s", err.Error())
-	}
-
-	// Ensure we create the info object if it doesn't exist
-	if data == nil {
-		data = make(map[string]string)
-	}
+	data := GetClusterInfo(p.KubeClientSet, p.Cloud)
 
-	data["clusterProfile"] = clusterProfile
-
-	// Include Product Reporting Flags with Cluster Info
-	writeReportingFlags(data)
-
-	// Include Thanos Offset Duration if Applicable
-	data["thanosEnabled"] = fmt.Sprintf("%t", thanos.IsEnabled())
-	if thanos.IsEnabled() {
-		data["thanosOffset"] = thanos.Offset()
-	}
-
-	w.Write(WrapData(data, err))
+	w.Write(WrapData(data, nil))
 }
 
 func (p *Accesses) GetServiceAccountStatus(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
@@ -932,6 +891,10 @@ func Initialize(additionalConfigWatchers ...ConfigWatchers) {
 	prometheus.MustRegister(StatefulsetCollector{
 		KubeClientSet: kubeClientset,
 	})
+	prometheus.MustRegister(ClusterInfoCollector{
+		KubeClientSet: kubeClientset,
+		Cloud:         cloudProvider,
+	})
 
 	// cache responses from model for a default of 5 minutes; clear expired responses every 10 minutes
 	outOfClusterCache := cache.New(time.Minute*5, time.Minute*10)

+ 69 - 0
pkg/prom/metrics.go

@@ -0,0 +1,69 @@
+package prom
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strings"
+)
+
+// AnyToLabels will create prometheus labels based on the fields of the interface
+// passed. Note that this method is quite expensive and should only be used when absolutely
+// necessary.
+func AnyToLabels(a interface{}) (map[string]string, error) {
+	val := reflect.ValueOf(a)
+	if val.Kind() == reflect.Map {
+		return MapToLabels(a), nil
+	}
+
+	b, e := json.Marshal(a)
+	if e != nil {
+		return nil, e
+	}
+
+	var m map[string]interface{}
+	e = json.Unmarshal(b, &m)
+	if e != nil {
+		return nil, e
+	}
+
+	return MapToLabels(m), nil
+}
+
+// MapToLabels accepts a map type, and will return a new map containing all the nested
+// fields separated by _ with string versions of the values.
+func MapToLabels(m interface{}) map[string]string {
+	val := reflect.ValueOf(m)
+	if val.Kind() != reflect.Map {
+		return map[string]string{}
+	}
+
+	r := make(map[string]string)
+
+	for _, k := range val.MapKeys() {
+		key := strings.ToLower(k.String())
+		v := val.MapIndex(k).Interface()
+
+		switch v.(type) {
+		case uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, string, bool, float32, float64:
+			r[key] = fmt.Sprintf("%+v", v)
+
+		default:
+			mm := MapToLabels(v)
+			for kk, vv := range mm {
+				r[fmt.Sprintf("%s_%s", key, kk)] = vv
+			}
+		}
+	}
+
+	return r
+}
+
+// LabelNamesFrom accepts a mapping of labels to values and returns the label names.
+func LabelNamesFrom(labels map[string]string) []string {
+	keys := []string{}
+	for key := range labels {
+		keys = append(keys, key)
+	}
+	return keys
+}

+ 28 - 0
test/clusterinfo_test.go

@@ -0,0 +1,28 @@
+package test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/kubecost/cost-model/pkg/prom"
+)
+
+func TestClusterInfoLabels(t *testing.T) {
+	expected := map[string]bool{"clusterprofile": true, "errorreporting": true, "id": true, "logcollection": true, "name": true, "productanalytics": true, "provider": true, "provisioner": true, "remotereadenabled": true, "thanosenabled": true, "valuesreporting": true, "version": true}
+	clusterInfo := `{"clusterProfile":"production","errorReporting":"true","id":"cluster-one","logCollection":"true","name":"bolt-3","productAnalytics":"true","provider":"GCP","provisioner":"GKE","remoteReadEnabled":"false","thanosEnabled":"false","valuesReporting":"true","version":"1.14+"}`
+
+	var m map[string]interface{}
+	err := json.Unmarshal([]byte(clusterInfo), &m)
+	if err != nil {
+		t.Errorf("Error: %s", err)
+		return
+	}
+
+	labels := prom.MapToLabels(m)
+	for k := range expected {
+		if _, ok := labels[k]; !ok {
+			t.Errorf("Failed to locate key: \"%s\" in labels.", k)
+			return
+		}
+	}
+}