Skip to content

Commit

Permalink
pkg/customresourcestate implement info and stateSet metric type and r…
Browse files Browse the repository at this point in the history
…efactor configuration file

* Adds detection of booleans in string format to getNum.
* Refactors configuration file to allow definition of different metric types
  having different configuration variables.
* Refactor order of types and funcs in pkg/customersourcestate.
* Allows info and stateSet metrics to iterate over arrays.
* Adds `nilIsZero` config variable to gauge to indicate non-existing values to tread as 0 value instead of returning an error.
* Skip adding a label instead of setting value to `<nil>`.

crd allow info and stateSet metrics to expose values from arrays
  • Loading branch information
chrischdi committed Aug 16, 2022
1 parent 4bb1b38 commit 0cfbf0e
Show file tree
Hide file tree
Showing 7 changed files with 616 additions and 323 deletions.
174 changes: 95 additions & 79 deletions pkg/customresourcestate/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,78 @@ import (
"strings"

"github.com/gobuffalo/flect"

"k8s.io/klog/v2"

"k8s.io/kube-state-metrics/v2/pkg/customresource"
)

// Metrics is the top level configuration object.
type Metrics struct {
Spec MetricsSpec `yaml:"spec" json:"spec"`
}

// MetricsSpec is the configuration describing the custom resource state metrics to generate.
type MetricsSpec struct {
// Resources is the list of custom resources to be monitored. A resource with the same GroupVersionKind may appear
// multiple times (e.g., to customize the namespace or subsystem,) but will incur additional overhead.
Resources []Resource `yaml:"resources" json:"resources"`
}

// Resource configures a custom resource for metric generation.
type Resource struct {
// MetricNamePrefix defines a prefix for all metrics of the resource.
// It defaults to the GroupVersionKind string, with invalid characters replaced by _.
// If set to "_", no prefix will be added.
// Example: If GroupVersionKind is "my-team.io/v1/MyResource", MetricNamePrefix will be "my_team_io_v1_MyResource".
MetricNamePrefix string `yaml:"metricNamePrefix" json:"metricNamePrefix"`

// GroupVersionKind of the custom resource to be monitored.
GroupVersionKind GroupVersionKind `yaml:"groupVersionKind" json:"groupVersionKind"`

// Labels are added to all metrics. If the same key is used in a metric, the value from the metric will overwrite the value here.
Labels `yaml:",inline" json:",inline"`

// Metrics are the custom resource fields to be collected.
Metrics []Generator `yaml:"metrics" json:"metrics"`
// ErrorLogV defines the verbosity threshold for errors logged for this resource.
ErrorLogV klog.Level `yaml:"errorLogV" json:"errorLogV"`

// ResourcePlural sets the plural name of the resource. Defaults to the plural version of the Kind according to flect.Pluralize.
ResourcePlural string `yaml:"resourcePlural" json:"resourcePlural"`
}

// GetMetricNamePrefix returns the prefix to use for metrics.
func (r Resource) GetMetricNamePrefix() string {
switch r.MetricNamePrefix {
case "":
return strings.NewReplacer(
"/", "_",
".", "_",
"-", "_",
).Replace(fmt.Sprintf("%s_%s_%s", r.GroupVersionKind.Group, r.GroupVersionKind.Version, r.GroupVersionKind.Kind))
case "_":
return ""
default:
return r.MetricNamePrefix
}
}

// GetResourceName returns the lowercase, plural form of the resource Kind. This is ResourcePlural if it is set.
func (r Resource) GetResourceName() string {
if r.ResourcePlural != "" {
return r.ResourcePlural
}
// kubebuilder default:
return strings.ToLower(flect.Pluralize(r.GroupVersionKind.Kind))
}

// GroupVersionKind is the Kubernetes group, version, and kind of a resource.
type GroupVersionKind struct {
Group string `yaml:"group" json:"group"`
Version string `yaml:"version" json:"version"`
Kind string `yaml:"kind" json:"kind"`
}

// MetricPer targets a Path that may be a single value, array, or object. Arrays and objects will generate a metric per element.
type MetricPer struct {
// Path is the path to the value to generate metric(s) for.
Path []string `yaml:"path" json:"path"`
// ValueFrom is the path to a numeric field under Path that will be the metric value.
ValueFrom []string `yaml:"valueFrom" json:"valueFrom"`
// LabelFromKey adds a label with the given name if Path is an object. The label value will be the object key.
LabelFromKey string `yaml:"labelFromKey" json:"labelFromKey"`
// LabelsFromPath adds additional labels where the value of the label is taken from a field under Path.
LabelsFromPath map[string][]string `yaml:"labelsFromPath" json:"labelsFromPath"`
}

// Labels is common configuration of labels to add to metrics.
type Labels struct {
// CommonLabels are added to all metrics.
Expand Down Expand Up @@ -82,82 +131,49 @@ type Generator struct {
// Help text for the metric.
Help string `yaml:"help" json:"help"`
// Each targets a value or values from the resource.
Each MetricPer `yaml:"each" json:"each"`
Each Metric `yaml:"each" json:"each"`

// Labels are added to all metrics. Labels from Each will overwrite these if using the same key.
Labels `yaml:",inline"` // json will inline because it is already tagged
Labels `yaml:",inline" json:",inline"` // json will inline because it is already tagged
// ErrorLogV defines the verbosity threshold for errors logged for this metric. Must be non-zero to override the resource setting.
ErrorLogV klog.Level `yaml:"errorLogV" json:"errorLogV"`
}

// Resource configures a custom resource for metric generation.
type Resource struct {
// Namespace is an optional prefix for all metrics. Defaults to "kube" if not set. If set to "_", no namespace will be added.
// The combination of Namespace and Subsystem will be prefixed to all metrics generated for this resource.
// e.g., if Namespace is "kube" and Subsystem is "myteam_io_v1_MyResource", all metrics will be prefixed with "kube_myteam_io_v1_MyResource_".
Namespace string `yaml:"namespace" json:"namespace"`
// Subsystem defaults to the GroupVersionKind string, with invalid character replaced with _. If set to "_", no subsystem will be added.
// e.g., if GroupVersionKind is "myteam.io/v1/MyResource", Subsystem will be "myteam_io_v1_MyResource".
Subsystem string `yaml:"subsystem" json:"subsystem"`

// GroupVersionKind of the custom resource to be monitored.
GroupVersionKind GroupVersionKind `yaml:"groupVersionKind" json:"groupVersionKind"`

// Labels are added to all metrics. If the same key is used in a metric, the value from the metric will overwrite the value here.
Labels `yaml:",inline"`

// Metrics are the custom resource fields to be collected.
Metrics []Generator `yaml:"metrics" json:"metrics"`
// ErrorLogV defines the verbosity threshold for errors logged for this resource.
ErrorLogV klog.Level `yaml:"errorLogV" json:"errorLogV"`

// ResourcePlural sets the plural name of the resource. Defaults to the plural version of the Kind according to flect.Pluralize.
ResourcePlural string `yaml:"resourcePlural" json:"resourcePlural"`
// Metric defines a metric to expose.
// +union
type Metric struct {
// Type defines the type of the metric.
// +unionDiscriminator
Type MetricType `yaml:"type" json:"type"`

// Gauge defines a gauge metric.
// +optional
Gauge *MetricGauge `yaml:"gauge" json:"gauge"`
// StateSet defines a state set metric.
// +optional
StateSet *MetricStateSet `yaml:"stateSet" json:"stateSet"`
// Info defines a info metric.
// +optional
Info *MetricInfo `yaml:"info" json:"info"`
}

// GetNamespace returns the namespace prefix to use for metrics.
func (r Resource) GetNamespace() string {
if r.Namespace == "" {
return "kube"
}
if r.Namespace == "_" {
return ""
}
return r.Namespace
// ConfigDecoder is for use with FromConfig.
type ConfigDecoder interface {
Decode(v interface{}) (err error)
}

// GetSubsystem returns the subsystem prefix to use for metrics (will be joined between namespace and the metric name).
func (r Resource) GetSubsystem() string {
if r.Subsystem == "" {
return strings.NewReplacer(
"/", "_",
".", "_",
"-", "_",
).Replace(fmt.Sprintf("%s_%s_%s", r.GroupVersionKind.Group, r.GroupVersionKind.Version, r.GroupVersionKind.Kind))
}
if r.Subsystem == "_" {
return ""
// FromConfig decodes a configuration source into a slice of customresource.RegistryFactory that are ready to use.
func FromConfig(decoder ConfigDecoder) (factories []customresource.RegistryFactory, err error) {
var crconfig Metrics
if err := decoder.Decode(&crconfig); err != nil {
return nil, fmt.Errorf("failed to parse Custom Resource State metrics: %w", err)
}
return r.Subsystem
}

// GetResourceName returns the lowercase, plural form of the resource Kind. This is ResourcePlural if it is set.
func (r Resource) GetResourceName() string {
if r.ResourcePlural != "" {
return r.ResourcePlural
for _, resource := range crconfig.Spec.Resources {
factory, err := NewCustomResourceMetrics(resource)
if err != nil {
return nil, fmt.Errorf("failed to create metrics factory for %s: %w", resource.GroupVersionKind, err)
}
factories = append(factories, factory)
}
// kubebuilder default:
return strings.ToLower(flect.Pluralize(r.GroupVersionKind.Kind))
}

// Metrics is the top level configuration object.
type Metrics struct {
Spec MetricsSpec `yaml:"spec" json:"spec"`
}

// MetricsSpec is the configuration describing the custom resource state metrics to generate.
type MetricsSpec struct {
// Resources is the list of custom resources to be monitored. A resource with the same GroupVersionKind may appear
// multiple times (e.g., to customize the namespace or subsystem,) but will incur additional overhead.
Resources []Resource `yaml:"resources" json:"resources"`
return factories, nil
}
67 changes: 67 additions & 0 deletions pkg/customresourcestate/config_metrics_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright 2021 The Kubernetes Authors 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 customresourcestate

// MetricType is the type of a metric.
type MetricType string

// Supported metric types.
const (
MetricTypeGauge MetricType = "Gauge"
MetricTypeStateSet MetricType = "StateSet"
MetricTypeInfo MetricType = "Info"
)

// MetricMeta are variables which may used for any metric type.
type MetricMeta struct {
// LabelsFromPath adds additional labels where the value of the label is taken from a field under Path.
LabelsFromPath map[string][]string `yaml:"labelsFromPath" json:"labelsFromPath"`
// Path is the path to to generate metric(s) for.
Path []string `yaml:"path" json:"path"`
}

// MetricGauge targets a Path that may be a single value, array, or object. Arrays and objects will generate a metric per element.
// Ref: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#gauge
type MetricGauge struct {
MetricMeta `yaml:",inline" json:",inline"`

// ValueFrom is the path to a numeric field under Path that will be the metric value.
ValueFrom []string `yaml:"valueFrom" json:"valueFrom"`
// LabelFromKey adds a label with the given name if Path is an object. The label value will be the object key.
LabelFromKey string `yaml:"labelFromKey" json:"labelFromKey"`
// NilIsZero indicates that if a value is nil it will be treated as zero value.
NilIsZero bool `yaml:"nilIsZero" json:"nilIsZero"`
}

// MetricInfo is a metric which is used to expose textual information.
// Ref: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#info
type MetricInfo struct {
MetricMeta `yaml:",inline" json:",inline"`
}

// MetricStateSet is a metric which represent a series of related boolean values, also called a bitset.
// Ref: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#stateset
type MetricStateSet struct {
MetricMeta `yaml:",inline" json:",inline"`

// List is the list of values to expose a value for.
List []string `yaml:"list" json:"list"`
// LabelName is the key of the label which is used for each entry in List to expose the value.
LabelName string `yaml:"labelName" json:"labelName"`
// ValueFrom is the subpath to compare the list to.
ValueFrom []string `yaml:"valueFrom" json:"valueFrom"`
}
8 changes: 4 additions & 4 deletions pkg/customresourcestate/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,21 @@ func Test_Metrics_deserialization(t *testing.T) {
assert.Equal(t, "active_count", m.Spec.Resources[0].Metrics[0].Name)

t.Run("can create resource factory", func(t *testing.T) {
rf, err := NewFieldMetrics(m.Spec.Resources[0])
rf, err := NewCustomResourceMetrics(m.Spec.Resources[0])
assert.NoError(t, err)

t.Run("labels are merged", func(t *testing.T) {
assert.Equal(t, map[string]string{
"name": mustCompilePath(t, "metadata", "name").String(),
}, toPaths(rf.(*fieldMetrics).Families[1].LabelFromPath))
}, toPaths(rf.(*customResourceMetrics).Families[1].LabelFromPath))
})

t.Run("errorLogV", func(t *testing.T) {
assert.Equal(t, klog.Level(5), rf.(*fieldMetrics).Families[1].ErrorLogV)
assert.Equal(t, klog.Level(5), rf.(*customResourceMetrics).Families[1].ErrorLogV)
})

t.Run("resource name", func(t *testing.T) {
assert.Equal(t, rf.(*fieldMetrics).ResourceName, "foos")
assert.Equal(t, rf.(*customResourceMetrics).ResourceName, "foos")
})
})
}
Expand Down
Loading

0 comments on commit 0cfbf0e

Please sign in to comment.