Files
troubleshoot/pkg/collect/k8s_metrics.go
Ahmed Mousa 620fa75eb5 feat: add k8s custom metrics collector (#1174)
Collector that collects custom k8s metrics from custom.metrics.k8s.io/v1beta1/ and saves them in the bundle under the /metrics directory
2023-06-09 10:26:03 +01:00

131 lines
4.3 KiB
Go

package collect
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"path/filepath"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"k8s.io/metrics/pkg/apis/custom_metrics"
)
const (
namespaceSingular = "namespace"
namespacePlural = "namespaces"
urlBase = "/apis/custom.metrics.k8s.io/v1beta1"
metricsErrorFile = "metrics/errors.json"
)
type CollectMetrics struct {
Collector *troubleshootv1beta2.CustomMetrics
BundlePath string
ClientConfig *rest.Config
Client kubernetes.Interface
Context context.Context
RBACErrors
}
func (c *CollectMetrics) Title() string {
return getCollectorName(c)
}
func (c *CollectMetrics) IsExcluded() (bool, error) {
return isExcluded(c.Collector.Exclude)
}
func (c *CollectMetrics) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
output := NewResult()
resultLists := make(map[string][]custom_metrics.MetricValue)
errorsList := make([]string, 0)
for _, metricRequest := range c.Collector.MetricRequests {
klog.V(2).Infof("Getting metric values: %+v\n", metricRequest.ResourceMetricName)
endpoint, metricName, err := constructEndpoint(metricRequest)
if err != nil {
errorsList = append(errorsList, errors.Wrapf(err, "could not construct endpoint for %s", metricRequest.ResourceMetricName).Error())
continue
}
klog.V(2).Infof("Querying: %+v\n", endpoint)
response, err := c.Client.CoreV1().RESTClient().Get().AbsPath(endpoint).DoRaw(c.Context)
if err != nil {
errorsList = append(errorsList, errors.Wrapf(err, "could not query endpoint %s", endpoint).Error())
continue
}
metricsValues := custom_metrics.MetricValueList{}
json.Unmarshal(response, &metricsValues)
// metrics
// |_ <resource_type>
// |_ <metric_name>
// |_ <namespace>.json or <non_namespaced_object>.json
var path []string
for _, item := range metricsValues.Items {
if item.DescribedObject.Namespace != "" {
path = []string{"metrics", item.DescribedObject.Kind, metricName, fmt.Sprintf("%s.json", item.DescribedObject.Namespace)}
} else {
path = []string{"metrics", item.DescribedObject.Kind, metricName, fmt.Sprintf("%s.json", item.DescribedObject.Name)}
}
filePath := filepath.Join(path...)
if _, ok := resultLists[filePath]; !ok {
resultLists[filePath] = make([]custom_metrics.MetricValue, 0)
}
resultLists[filePath] = append(resultLists[filePath], item)
}
}
// Construct output.
for relativePath, list := range resultLists {
payload, err := json.MarshalIndent(list, "", " ")
if err != nil {
klog.V(2).Infof("Could not parse for: %+v\n", relativePath)
errorsList = append(errorsList, errors.Wrapf(err, "could not format readings for %s", relativePath).Error())
}
output.SaveResult(c.BundlePath, relativePath, bytes.NewBuffer(payload))
}
errPayload := marshalErrors(errorsList)
output.SaveResult(c.BundlePath, metricsErrorFile, errPayload)
return output, nil
}
func constructEndpoint(metricRequest troubleshootv1beta2.MetricRequest) (string, string, error) {
metricNameComponents := strings.Split(metricRequest.ResourceMetricName, "/")
if len(metricNameComponents) != 2 {
return "", "", errors.New("wrong metric name format %s")
}
objectType := metricNameComponents[0]
// Namespace related metrics are grouped under singular format "namespace/"
// unlike other resources.
if objectType == namespacePlural {
objectType = namespaceSingular
}
metricName := metricNameComponents[1]
objectSelector := "*"
if metricRequest.ObjectName != "" {
objectSelector = metricRequest.ObjectName
}
var endpoint string
var err error
if metricRequest.Namespace != "" {
// namespaced objects
// endpoint <resource_type>/namespaces/<namespace>/<resrouce_name or *>/<metric>
endpoint, err = url.JoinPath(urlBase, namespacePlural, metricRequest.Namespace, objectType, objectSelector, metricName)
if err != nil {
return "", "", errors.Wrap(err, "could not construct url")
}
} else {
// non-namespaced objects
// endpoint <resource_type>/<resrouce_name or *>/<metric>
endpoint, err = url.JoinPath(urlBase, objectType, objectSelector, metricName)
if err != nil {
return "", "", errors.Wrap(err, "could not construct url")
}
}
return endpoint, metricName, nil
}