Skip to content

Commit

Permalink
feat: add k8s custom metrics collector (#1174)
Browse files Browse the repository at this point in the history
Collector that collects custom k8s metrics from custom.metrics.k8s.io/v1beta1/ and saves them in the bundle under the /metrics directory
  • Loading branch information
ahmedElqutb authored Jun 9, 2023
1 parent 60d5b68 commit 620fa75
Show file tree
Hide file tree
Showing 13 changed files with 483 additions and 0 deletions.
30 changes: 30 additions & 0 deletions config/crds/troubleshoot.sh_collectors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,36 @@ spec:
- image
- namespace
type: object
customMetrics:
properties:
collectorName:
type: string
exclude:
type: BoolString
metricRequests:
items:
description: MetricRequest the details of the MetricValuesList
to be retrieved
properties:
namespace:
description: Namespace for which to collect the metric
values, empty for non-namespaces resources.
type: string
objectName:
description: ObjectName for which to collect metric
values, all resources when empty. Note that for
namespaced resources a Namespace has to be supplied
regardless.
type: string
resourceMetricName:
description: ResourceMetricName name of the MetricValueList
as per the APIResourceList from custom.metrics.k8s.io/v1beta1
type: string
required:
- resourceMetricName
type: object
type: array
type: object
data:
properties:
collectorName:
Expand Down
30 changes: 30 additions & 0 deletions config/crds/troubleshoot.sh_preflights.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,36 @@ spec:
- image
- namespace
type: object
customMetrics:
properties:
collectorName:
type: string
exclude:
type: BoolString
metricRequests:
items:
description: MetricRequest the details of the MetricValuesList
to be retrieved
properties:
namespace:
description: Namespace for which to collect the metric
values, empty for non-namespaces resources.
type: string
objectName:
description: ObjectName for which to collect metric
values, all resources when empty. Note that for
namespaced resources a Namespace has to be supplied
regardless.
type: string
resourceMetricName:
description: ResourceMetricName name of the MetricValueList
as per the APIResourceList from custom.metrics.k8s.io/v1beta1
type: string
required:
- resourceMetricName
type: object
type: array
type: object
data:
properties:
collectorName:
Expand Down
30 changes: 30 additions & 0 deletions config/crds/troubleshoot.sh_supportbundles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,36 @@ spec:
- image
- namespace
type: object
customMetrics:
properties:
collectorName:
type: string
exclude:
type: BoolString
metricRequests:
items:
description: MetricRequest the details of the MetricValuesList
to be retrieved
properties:
namespace:
description: Namespace for which to collect the metric
values, empty for non-namespaces resources.
type: string
objectName:
description: ObjectName for which to collect metric
values, all resources when empty. Note that for
namespaced resources a Namespace has to be supplied
regardless.
type: string
resourceMetricName:
description: ResourceMetricName name of the MetricValueList
as per the APIResourceList from custom.metrics.k8s.io/v1beta1
type: string
required:
- resourceMetricName
type: object
type: array
type: object
data:
properties:
collectorName:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/metrics v0.27.2
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
periph.io/x/host/v3 v3.8.2
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,8 @@ k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
k8s.io/metrics v0.27.2 h1:TD6z3dhhN9bgg5YkbTh72bPiC1BsxipBLPBWyC3VQAU=
k8s.io/metrics v0.27.2/go.mod h1:v3OT7U0DBvoAzWVzGZWQhdV4qsRJWchzs/LeVN8bhW4=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go v1.2.3 h1:v8PJl+gEAntI1pJ/LCrDgsuk+1PKVavVEPsYIHFE5uY=
Expand Down
18 changes: 18 additions & 0 deletions pkg/apis/troubleshoot/v1beta2/collector_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ type ClusterResources struct {
IgnoreRBAC bool `json:"ignoreRBAC,omitempty" yaml:"ignoreRBAC"`
}

// MetricRequest the details of the MetricValuesList to be retrieved
type MetricRequest struct {
// Namespace for which to collect the metric values, empty for non-namespaces resources.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
// ObjectName for which to collect metric values, all resources when empty.
// Note that for namespaced resources a Namespace has to be supplied regardless.
ObjectName string `json:"objectName,omitempty" yaml:"objectName,omitempty"`
// ResourceMetricName name of the MetricValueList as per the APIResourceList from
// custom.metrics.k8s.io/v1beta1
ResourceMetricName string `json:"resourceMetricName" yaml:"resourceMetricName"`
}

type CustomMetrics struct {
CollectorMeta `json:",inline" yaml:",inline"`
MetricRequests []MetricRequest `json:"metricRequests,omitempty" yaml:"metricRequests,omitempty"`
}

type Secret struct {
CollectorMeta `json:",inline" yaml:",inline"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Expand Down Expand Up @@ -231,6 +248,7 @@ type Collect struct {
ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty" yaml:"clusterInfo,omitempty"`
ClusterResources *ClusterResources `json:"clusterResources,omitempty" yaml:"clusterResources,omitempty"`
Secret *Secret `json:"secret,omitempty" yaml:"secret,omitempty"`
CustomMetrics *CustomMetrics `json:"customMetrics,omitempty" yaml:"customMetrics,omitempty"`
ConfigMap *ConfigMap `json:"configMap,omitempty" yaml:"configMap,omitempty"`
Logs *Logs `json:"logs,omitempty" yaml:"logs,omitempty"`
Run *Run `json:"run,omitempty" yaml:"run,omitempty"`
Expand Down
41 changes: 41 additions & 0 deletions pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkg/collect/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, nam
return &CollectClusterInfo{collector.ClusterInfo, bundlePath, namespace, clientConfig, RBACErrors}, true
case collector.ClusterResources != nil:
return &CollectClusterResources{collector.ClusterResources, bundlePath, namespace, clientConfig, RBACErrors}, true
case collector.CustomMetrics != nil:
return &CollectMetrics{collector.CustomMetrics, bundlePath, clientConfig, client, ctx, RBACErrors}, true
case collector.Secret != nil:
return &CollectSecret{collector.Secret, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
case collector.ConfigMap != nil:
Expand Down Expand Up @@ -116,6 +118,9 @@ func getCollectorName(c interface{}) string {
collector = "cluster-info"
case *CollectClusterResources:
collector = "cluster-resources"
case *CollectMetrics:
collector = "custom-metrics"
name = v.Collector.CollectorName
case *CollectSecret:
collector = "secret"
name = v.Collector.CollectorName
Expand Down
130 changes: 130 additions & 0 deletions pkg/collect/k8s_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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
}
Loading

0 comments on commit 620fa75

Please sign in to comment.