|
|
@@ -2,182 +2,22 @@ package costmodel
|
|
|
|
|
|
import (
|
|
|
"fmt"
|
|
|
- "math"
|
|
|
- "strconv"
|
|
|
- "strings"
|
|
|
|
|
|
costAnalyzerCloud "github.com/kubecost/cost-model/pkg/cloud"
|
|
|
+ "github.com/kubecost/cost-model/pkg/log"
|
|
|
"github.com/kubecost/cost-model/pkg/prom"
|
|
|
- "github.com/kubecost/cost-model/pkg/util"
|
|
|
- "k8s.io/klog"
|
|
|
)
|
|
|
|
|
|
-// PromQueryResult contains a single result from a prometheus query
|
|
|
-type PromQueryResult struct {
|
|
|
- Metric map[string]interface{}
|
|
|
- Values []*util.Vector
|
|
|
-}
|
|
|
-
|
|
|
-func (pqr *PromQueryResult) GetString(field string) (string, error) {
|
|
|
- f, ok := pqr.Metric[field]
|
|
|
- if !ok {
|
|
|
- return "", fmt.Errorf("%s field does not exist in data result vector", field)
|
|
|
- }
|
|
|
-
|
|
|
- strField, ok := f.(string)
|
|
|
- if !ok {
|
|
|
- return "", fmt.Errorf("%s field is improperly formatted", field)
|
|
|
- }
|
|
|
-
|
|
|
- return strField, nil
|
|
|
-}
|
|
|
-
|
|
|
-func (pqr *PromQueryResult) GetLabels() map[string]string {
|
|
|
- result := make(map[string]string)
|
|
|
-
|
|
|
- // Find All keys with prefix label_, remove prefix, add to labels
|
|
|
- for k, v := range pqr.Metric {
|
|
|
- if !strings.HasPrefix(k, "label_") {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- label := k[6:]
|
|
|
- value, ok := v.(string)
|
|
|
- if !ok {
|
|
|
- klog.V(3).Infof("Failed to parse label value for label: %s", label)
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- result[label] = value
|
|
|
- }
|
|
|
-
|
|
|
- return result
|
|
|
-}
|
|
|
-
|
|
|
-// NewQueryResults accepts the raw prometheus query result and returns an array of
|
|
|
-// PromQueryResult objects
|
|
|
-func NewQueryResults(queryResult interface{}) ([]*PromQueryResult, error) {
|
|
|
- var result []*PromQueryResult
|
|
|
- if queryResult == nil {
|
|
|
- return nil, prom.NewCommError("nil queryResult")
|
|
|
- }
|
|
|
- data, ok := queryResult.(map[string]interface{})["data"]
|
|
|
- if !ok {
|
|
|
- e, err := wrapPrometheusError(queryResult)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
- return nil, fmt.Errorf(e)
|
|
|
- }
|
|
|
-
|
|
|
- // Deep Check for proper formatting
|
|
|
- d, ok := data.(map[string]interface{})
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Data field improperly formatted in prometheus repsonse")
|
|
|
- }
|
|
|
- resultData, ok := d["result"]
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Result field not present in prometheus response")
|
|
|
- }
|
|
|
- resultsData, ok := resultData.([]interface{})
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Result field improperly formatted in prometheus response")
|
|
|
- }
|
|
|
-
|
|
|
- // Scan Results
|
|
|
- for _, val := range resultsData {
|
|
|
- resultInterface, ok := val.(map[string]interface{})
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Result is improperly formatted")
|
|
|
- }
|
|
|
-
|
|
|
- metricInterface, ok := resultInterface["metric"]
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Metric field does not exist in data result vector")
|
|
|
- }
|
|
|
- metricMap, ok := metricInterface.(map[string]interface{})
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Metric field is improperly formatted")
|
|
|
- }
|
|
|
-
|
|
|
- // Wrap execution of this lazily in case the data is not used
|
|
|
- labels := func() string { return labelsForMetric(metricMap) }
|
|
|
-
|
|
|
- // Determine if the result is a ranged data set or single value
|
|
|
- _, isRange := resultInterface["values"]
|
|
|
-
|
|
|
- var vectors []*util.Vector
|
|
|
- if !isRange {
|
|
|
- dataPoint, ok := resultInterface["value"]
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Value field does not exist in data result vector")
|
|
|
- }
|
|
|
-
|
|
|
- v, err := parseDataPoint(dataPoint, labels)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
- vectors = append(vectors, v)
|
|
|
- } else {
|
|
|
- values, ok := resultInterface["values"].([]interface{})
|
|
|
- if !ok {
|
|
|
- return nil, fmt.Errorf("Values field is improperly formatted")
|
|
|
- }
|
|
|
-
|
|
|
- for _, value := range values {
|
|
|
- v, err := parseDataPoint(value, labels)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
-
|
|
|
- vectors = append(vectors, v)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- result = append(result, &PromQueryResult{
|
|
|
- Metric: metricMap,
|
|
|
- Values: vectors,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- return result, nil
|
|
|
-}
|
|
|
-
|
|
|
-func parseDataPoint(dataPoint interface{}, labels func() string) (*util.Vector, error) {
|
|
|
- value, ok := dataPoint.([]interface{})
|
|
|
- if !ok || len(value) != 2 {
|
|
|
- return nil, fmt.Errorf("Improperly formatted datapoint from Prometheus")
|
|
|
- }
|
|
|
-
|
|
|
- strVal := value[1].(string)
|
|
|
- v, err := strconv.ParseFloat(strVal, 64)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
-
|
|
|
- // Test for +Inf and -Inf (sign: 0), Test for NaN
|
|
|
- if math.IsInf(v, 0) {
|
|
|
- klog.V(1).Infof("[Warning] Found Inf value parsing vector data point for metric: %s", labels())
|
|
|
- v = 0.0
|
|
|
- } else if math.IsNaN(v) {
|
|
|
- klog.V(1).Infof("[Warning] Found NaN value parsing vector data point for metric: %s", labels())
|
|
|
- v = 0.0
|
|
|
- }
|
|
|
-
|
|
|
- return &util.Vector{
|
|
|
- Timestamp: math.Round(value[0].(float64)/10) * 10,
|
|
|
- Value: v,
|
|
|
- }, nil
|
|
|
-}
|
|
|
-
|
|
|
func GetPVInfo(qr interface{}, defaultClusterID string) (map[string]*PersistentVolumeClaimData, error) {
|
|
|
toReturn := make(map[string]*PersistentVolumeClaimData)
|
|
|
- result, err := NewQueryResults(qr)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of PVInfo
|
|
|
+ result, err := prom.NewQueryResults("PVInfo", qr)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
clusterID, err := val.GetString("cluster_id")
|
|
|
if clusterID == "" {
|
|
|
clusterID = defaultClusterID
|
|
|
@@ -195,14 +35,14 @@ func GetPVInfo(qr interface{}, defaultClusterID string) (map[string]*PersistentV
|
|
|
|
|
|
volumeName, err := val.GetString("volumename")
|
|
|
if err != nil {
|
|
|
- klog.V(4).Infof("[Warning] Unfulfilled claim %s: volumename field does not exist in data result vector", pvcName)
|
|
|
+ log.Debugf("Unfulfilled claim %s: volumename field does not exist in data result vector", pvcName)
|
|
|
volumeName = ""
|
|
|
}
|
|
|
|
|
|
pvClass, err := val.GetString("storageclass")
|
|
|
if err != nil {
|
|
|
// TODO: We need to look up the actual PV and PV capacity. For now just proceed with "".
|
|
|
- klog.V(2).Infof("[Warning] Storage Class not found for claim \"%s/%s\".", ns, pvcName)
|
|
|
+ log.Warningf("Storage Class not found for claim \"%s/%s\".", ns, pvcName)
|
|
|
pvClass = ""
|
|
|
}
|
|
|
|
|
|
@@ -222,12 +62,14 @@ func GetPVInfo(qr interface{}, defaultClusterID string) (map[string]*PersistentV
|
|
|
|
|
|
func GetPVAllocationMetrics(queryResult interface{}, defaultClusterID string) (map[string][]*PersistentVolumeClaimData, error) {
|
|
|
toReturn := make(map[string][]*PersistentVolumeClaimData)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of PVAllocationMetrics
|
|
|
+ result, err := prom.NewQueryResults("PVAllocationMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
clusterID, err := val.GetString("cluster_id")
|
|
|
if clusterID == "" {
|
|
|
clusterID = defaultClusterID
|
|
|
@@ -250,7 +92,7 @@ func GetPVAllocationMetrics(queryResult interface{}, defaultClusterID string) (m
|
|
|
|
|
|
pvName, err := val.GetString("persistentvolume")
|
|
|
if err != nil {
|
|
|
- klog.Infof("persistentvolume field does not exist for pv %s", pvcName) // This is possible for an unfulfilled claim
|
|
|
+ log.Warningf("persistentvolume field does not exist for pv %s", pvcName) // This is possible for an unfulfilled claim
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
@@ -272,12 +114,14 @@ func GetPVAllocationMetrics(queryResult interface{}, defaultClusterID string) (m
|
|
|
|
|
|
func GetPVCostMetrics(queryResult interface{}, defaultClusterID string) (map[string]*costAnalyzerCloud.PV, error) {
|
|
|
toReturn := make(map[string]*costAnalyzerCloud.PV)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of PVCostMetrics
|
|
|
+ result, err := prom.NewQueryResults("PVCostMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
clusterID, err := val.GetString("cluster_id")
|
|
|
if clusterID == "" {
|
|
|
clusterID = defaultClusterID
|
|
|
@@ -299,12 +143,14 @@ func GetPVCostMetrics(queryResult interface{}, defaultClusterID string) (map[str
|
|
|
|
|
|
func GetNamespaceLabelsMetrics(queryResult interface{}, defaultClusterID string) (map[string]map[string]string, error) {
|
|
|
toReturn := make(map[string]map[string]string)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of NamespaceLabelsMetrics
|
|
|
+ result, err := prom.NewQueryResults("NamespaceLabelsMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
// We want Namespace and ClusterID for key generation purposes
|
|
|
ns, err := val.GetString("namespace")
|
|
|
if err != nil {
|
|
|
@@ -330,12 +176,14 @@ func GetNamespaceLabelsMetrics(queryResult interface{}, defaultClusterID string)
|
|
|
|
|
|
func GetPodLabelsMetrics(queryResult interface{}, defaultClusterID string) (map[string]map[string]string, error) {
|
|
|
toReturn := make(map[string]map[string]string)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of PodLabelsMetrics
|
|
|
+ result, err := prom.NewQueryResults("PodLabelsMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
// We want Pod, Namespace and ClusterID for key generation purposes
|
|
|
pod, err := val.GetString("pod")
|
|
|
if err != nil {
|
|
|
@@ -368,12 +216,14 @@ func GetPodLabelsMetrics(queryResult interface{}, defaultClusterID string) (map[
|
|
|
|
|
|
func GetStatefulsetMatchLabelsMetrics(queryResult interface{}, defaultClusterID string) (map[string]map[string]string, error) {
|
|
|
toReturn := make(map[string]map[string]string)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of StatefulsetMatchLabelsMetrics
|
|
|
+ result, err := prom.NewQueryResults("StatefulsetMatchLabelsMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
// We want Statefulset, Namespace and ClusterID for key generation purposes
|
|
|
ss, err := val.GetString("statefulSet")
|
|
|
if err != nil {
|
|
|
@@ -399,11 +249,13 @@ func GetStatefulsetMatchLabelsMetrics(queryResult interface{}, defaultClusterID
|
|
|
|
|
|
func GetPodDaemonsetsWithMetrics(queryResult interface{}, defaultClusterID string) (map[string]string, error) {
|
|
|
toReturn := make(map[string]string)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of PodDaemonsetsWithMetrics
|
|
|
+ result, err := prom.NewQueryResults("PodDaemonsetsWithMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
ds, err := val.GetString("owner_name")
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
@@ -433,12 +285,14 @@ func GetPodDaemonsetsWithMetrics(queryResult interface{}, defaultClusterID strin
|
|
|
|
|
|
func GetDeploymentMatchLabelsMetrics(queryResult interface{}, defaultClusterID string) (map[string]map[string]string, error) {
|
|
|
toReturn := make(map[string]map[string]string)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of DeploymentMatchLabelsMetrics
|
|
|
+ result, err := prom.NewQueryResults("DeploymentMatchLabelsMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
// We want Deployment, Namespace and ClusterID for key generation purposes
|
|
|
deployment, err := val.GetString("deployment")
|
|
|
if err != nil {
|
|
|
@@ -464,12 +318,14 @@ func GetDeploymentMatchLabelsMetrics(queryResult interface{}, defaultClusterID s
|
|
|
|
|
|
func GetServiceSelectorLabelsMetrics(queryResult interface{}, defaultClusterID string) (map[string]map[string]string, error) {
|
|
|
toReturn := make(map[string]map[string]string)
|
|
|
- result, err := NewQueryResults(queryResult)
|
|
|
+
|
|
|
+ // TODO: Pass actual query instead of ServiceSelectorLabelsMetrics
|
|
|
+ result, err := prom.NewQueryResults("ServiceSelectorLabelsMetrics", queryResult)
|
|
|
if err != nil {
|
|
|
return toReturn, err
|
|
|
}
|
|
|
|
|
|
- for _, val := range result {
|
|
|
+ for _, val := range result.Results {
|
|
|
// We want Service, Namespace and ClusterID for key generation purposes
|
|
|
service, err := val.GetString("service")
|
|
|
if err != nil {
|
|
|
@@ -492,12 +348,3 @@ func GetServiceSelectorLabelsMetrics(queryResult interface{}, defaultClusterID s
|
|
|
|
|
|
return toReturn, nil
|
|
|
}
|
|
|
-
|
|
|
-func labelsForMetric(metricMap map[string]interface{}) string {
|
|
|
- var pairs []string
|
|
|
- for k, v := range metricMap {
|
|
|
- pairs = append(pairs, fmt.Sprintf("%s: %+v", k, v))
|
|
|
- }
|
|
|
-
|
|
|
- return fmt.Sprintf("{%s}", strings.Join(pairs, ", "))
|
|
|
-}
|