diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 911ff7d0b1..9328a6f88f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,3 +207,24 @@ jobs: - name: End-to-end tests run: | make e2e + + ci-validate-stable-metrics-tests: + name: ci-validate-stable-metrics + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v4 + + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + id: go + + - name: Setup environment + run: | + make install-tools + + - name: End-to-end tests + run: | + make validate-stable-metrics diff --git a/Makefile b/Makefile index 016a024c24..88b0bc35a7 100644 --- a/Makefile +++ b/Makefile @@ -129,6 +129,12 @@ clean: e2e: ./tests/e2e.sh +validate-stable-metrics: + ./tests/validate-stability.sh + +fix-stable-metrics: + ./tests/fix-stability.sh + generate: build-local @echo ">> generating docs" @./scripts/generate-help-text.sh diff --git a/docs/design/validate-stable-metrics.md b/docs/design/validate-stable-metrics.md new file mode 100644 index 0000000000..5c6787b620 --- /dev/null +++ b/docs/design/validate-stable-metrics.md @@ -0,0 +1,85 @@ +# Kube-State-Metrics - Validate stable metrics Proposal + +--- + +Author: CatherineF-dev@ + +Date: 1. Jan 2024 + +--- + +## Glossary + +* STABLE metrics: it's same to kubernetes/kubernetes BETA metrics. It does + not allow breaking changes which break metrics queries. For example, changing + label name is not allowed. + +* Experimental metrics: it's same to kubernetes/kubernetes ALPHA metrics. It +allows breaking changes. For example, it allows changing label name. + +## Problem Statement + +Broken stable metrics bring overhead for down-stream users to migrate metrics +queries. + +## Goal + +The goal of this proposal is guaranteeing these for stable metrics: + +1. metric name not changed + +2. metric type not changed + +3. old metric labels is a subset of new metric labels + +## Status Quo + +Kubernetes/kubernetes has implemented stable metrics framework. It can not be +used in KSM repo directly, because kubernetes/kubernetes metrics are defined +using prometheus libraries while KSM metrics are defined using custom functions. + +## Proposal - validate stable metrics using static analysis + +1. Add a new funciton NewFamilyGeneratorWithStabilityV2 which has labels in its + parameter. It's easier for static analysis in step 2. + +2. Adapt stable metrics framework +into kube-state-metrics repo. + +2.1 Find stable metrics definitions. It finds all function calls with name NewFamilyGeneratorWithStabilityV2 where fourth paraemeter (StabilityLevel) is stable + +2.2 Extract metric name, labels, help, stability level from 2.1 + +``` +func createPodInitContainerStatusRestartsTotalFamilyGenerator() generator.FamilyGenerator { + return *generator.NewFamilyGeneratorWithStabilityV2( + "kube_pod_init_container_status_restarts_total", + "The number of restarts for the init container.", + metric.Counter, basemetrics.STABLE, + "", + []string{"container"}, # labels + ... +} +``` + +2.3 Export all stable metrics, with format + +``` +- name: kube_pod_init_container_status_restarts_total + help: The number of restarts for the init container. + type: Counter + stabilityLevel: STABLE + labels: + - container +``` + +2.4 Compare output in 2.3 with expected results tests/stablemetrics/testdata/test-stable-metrics-list.yaml + +## Alternatives + +### Validate exposed metrics in runtime + +Generated metrics are not complete. Some metrics are exposed when certain conditions +are met. + +## FAQ / Follow up improvements diff --git a/go.mod b/go.mod index b5022c05b4..09ea8bc50b 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.8.4 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.28.4 k8s.io/apimachinery v0.28.4 @@ -84,7 +85,6 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect diff --git a/internal/store/pod.go b/internal/store/pod.go index 09a83d171b..64ac307d8d 100644 --- a/internal/store/pod.go +++ b/internal/store/pod.go @@ -882,17 +882,17 @@ func createPodInitContainerStatusReadyFamilyGenerator() generator.FamilyGenerato } func createPodInitContainerStatusRestartsTotalFamilyGenerator() generator.FamilyGenerator { - return *generator.NewFamilyGeneratorWithStability( + return *generator.NewFamilyGeneratorWithStabilityV2( "kube_pod_init_container_status_restarts_total", "The number of restarts for the init container.", metric.Counter, basemetrics.STABLE, "", + []string{"container"}, wrapPodFunc(func(p *v1.Pod) *metric.Family { ms := make([]*metric.Metric, len(p.Status.InitContainerStatuses)) for i, cs := range p.Status.InitContainerStatuses { ms[i] = &metric.Metric{ - LabelKeys: []string{"container"}, LabelValues: []string{cs.Name}, Value: float64(cs.RestartCount), } diff --git a/pkg/metric_generator/generator.go b/pkg/metric_generator/generator.go index 2a2e30221f..87b2c3c185 100644 --- a/pkg/metric_generator/generator.go +++ b/pkg/metric_generator/generator.go @@ -39,6 +39,24 @@ type FamilyGenerator struct { GenerateFunc func(obj interface{}) *metric.Family } +// NewFamilyGeneratorWithStabilityV2 creates new FamilyGenerator instances with metric +// stabilityLevel and explicit labels. These explicit labels are used for verifying stable metrics. +func NewFamilyGeneratorWithStabilityV2(name string, help string, metricType metric.Type, stabilityLevel basemetrics.StabilityLevel, deprecatedVersion string, labels []string, generateFunc func(obj interface{}) *metric.Family) *FamilyGenerator { + f := &FamilyGenerator{ + Name: name, + Type: metricType, + Help: help, + OptIn: false, + StabilityLevel: stabilityLevel, + DeprecatedVersion: deprecatedVersion, + GenerateFunc: wrapLabels(generateFunc, labels), + } + if deprecatedVersion != "" { + f.Help = fmt.Sprintf("(Deprecated since %s) %s", deprecatedVersion, help) + } + return f +} + // NewFamilyGeneratorWithStability creates new FamilyGenerator instances with metric // stabilityLevel. func NewFamilyGeneratorWithStability(name string, help string, metricType metric.Type, stabilityLevel basemetrics.StabilityLevel, deprecatedVersion string, generateFunc func(obj interface{}) *metric.Family) *FamilyGenerator { @@ -57,6 +75,19 @@ func NewFamilyGeneratorWithStability(name string, help string, metricType metric return f } +func wrapLabels(generateFunc func(obj interface{}) *metric.Family, labels []string) func(obj interface{}) *metric.Family { + return func(obj interface{}) *metric.Family { + + metricFamily := generateFunc(obj) + + for _, m := range metricFamily.Metrics { + m.LabelKeys = append(m.LabelKeys, labels...) + } + + return metricFamily + } +} + // NewOptInFamilyGenerator creates new FamilyGenerator instances for opt-in metric families. func NewOptInFamilyGenerator(name string, help string, metricType metric.Type, stabilityLevel basemetrics.StabilityLevel, deprecatedVersion string, generateFunc func(obj interface{}) *metric.Family) *FamilyGenerator { f := NewFamilyGeneratorWithStability(name, help, metricType, stabilityLevel, diff --git a/tests/fix-stability.sh b/tests/fix-stability.sh new file mode 100755 index 0000000000..d666f167e5 --- /dev/null +++ b/tests/fix-stability.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -o errexit + +out_file='tests/stablemetrics/testdata/test-stable-metrics-list.yaml' +metric_file='internal/store/pod.go' + +go run \ + "tests/stablemetrics/main.go" \ + "tests/stablemetrics/decode_metric.go" \ + "tests/stablemetrics/find_stable_metric.go" \ + "tests/stablemetrics/error.go" \ + "tests/stablemetrics/metric.go" \ + -- \ + "${metric_file}" >"${out_file}" + + diff --git a/tests/stablemetrics/decode_metric.go b/tests/stablemetrics/decode_metric.go new file mode 100644 index 0000000000..06acfca487 --- /dev/null +++ b/tests/stablemetrics/decode_metric.go @@ -0,0 +1,173 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 main + +import ( + "go/ast" + "go/token" + "strings" + + "k8s.io/component-base/metrics" +) + +func decodeMetricCalls(fs []*ast.CallExpr, metricsImportName string, variables map[string]ast.Expr) ([]metric, []error) { + finder := metricDecoder{ + kubeMetricsImportName: metricsImportName, + variables: variables, + } + ms := make([]metric, 0, len(fs)) + errors := []error{} + for _, f := range fs { + m, err := finder.decodeNewMetricCall(f) + if err != nil { + errors = append(errors, err) + continue + } + if m != nil { + ms = append(ms, *m) + } + } + return ms, errors +} + +type metricDecoder struct { + kubeMetricsImportName string + variables map[string]ast.Expr +} + +func (c *metricDecoder) decodeNewMetricCall(fc *ast.CallExpr) (*metric, error) { + var m metric + var err error + + _, ok := fc.Fun.(*ast.SelectorExpr) + if !ok { + return nil, newDecodeErrorf(fc, errNotDirectCall) + } + m, err = c.decodeDesc(fc) + return &m, err +} + +func (c *metricDecoder) decodeDesc(ce *ast.CallExpr) (metric, error) { + m := &metric{} + + name, err := c.decodeString(ce.Args[0]) + if err != nil { + return *m, newDecodeErrorf(ce, errorDecodingString) + } + m.Name = *name + + help, err := c.decodeString(ce.Args[1]) + if err != nil { + return *m, newDecodeErrorf(ce, errorDecodingString) + } + m.Help = *help + + metricType, err := decodeStabilityLevel(ce.Args[2], "metric") + if err != nil { + return *m, newDecodeErrorf(ce, errorDecodingString) + } + m.Type = string(*metricType) + + sl, err := decodeStabilityLevel(ce.Args[3], "basemetrics") + if err != nil { + return *m, newDecodeErrorf(ce, "can't decode stability level") + } + + if sl != nil { + m.StabilityLevel = string(*sl) + } + + labels, err := c.decodeLabels(ce.Args[5]) + if err != nil { + return *m, newDecodeErrorf(ce, errorDecodingLabels) + } + m.Labels = labels + + return *m, nil +} + +func (c *metricDecoder) decodeString(expr ast.Expr) (*string, error) { + + switch e := expr.(type) { + case *ast.BasicLit: + value, err := stringValue(e) + if err != nil { + return nil, err + } + return &value, nil + } + return nil, newDecodeErrorf(expr, errorDecodingString) +} + +func (c *metricDecoder) decodeLabelsFromArray(exprs []ast.Expr) ([]string, error) { + retval := []string{} + for _, e := range exprs { + v, err := c.decodeString(e) + if err != nil || v == nil { + return nil, newDecodeErrorf(e, errNonStringAttribute) + } + retval = append(retval, *v) + } + + return retval, nil +} + +func (c *metricDecoder) decodeLabels(expr ast.Expr) ([]string, error) { + cl, ok := expr.(*ast.CompositeLit) + if !ok { + switch e := expr.(type) { + case *ast.Ident: + if e.Name == "nil" { + return []string{}, nil + } + variableExpr, found := c.variables[e.Name] + if !found { + return nil, newDecodeErrorf(expr, errorFindingVariableForLabels) + } + cl2, ok := variableExpr.(*ast.CompositeLit) + if !ok { + return nil, newDecodeErrorf(expr, errorFindingVariableForLabels) + } + cl = cl2 + } + } + return c.decodeLabelsFromArray(cl.Elts) +} + +func stringValue(bl *ast.BasicLit) (string, error) { + if bl.Kind != token.STRING { + return "", newDecodeErrorf(bl, errNonStringAttribute) + } + return strings.Trim(bl.Value, `"`), nil +} + +func decodeStabilityLevel(expr ast.Expr, metricsFrameworkImportName string) (*metrics.StabilityLevel, error) { + se, ok := expr.(*ast.SelectorExpr) + if !ok { + return nil, newDecodeErrorf(expr, errStabilityLevel) + } + s, ok := se.X.(*ast.Ident) + if !ok { + return nil, newDecodeErrorf(expr, errStabilityLevel) + } + if s.String() != metricsFrameworkImportName { + return nil, newDecodeErrorf(expr, errStabilityLevel) + } + + stability := metrics.StabilityLevel(se.Sel.Name) + return &stability, nil +} diff --git a/tests/stablemetrics/error.go b/tests/stablemetrics/error.go new file mode 100644 index 0000000000..0443f4bfef --- /dev/null +++ b/tests/stablemetrics/error.go @@ -0,0 +1,72 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 main + +import ( + "fmt" + "go/ast" + "go/token" +) + +const ( + errNotDirectCall = "Opts for STABLE metric was not directly passed to new metric function" + errPositionalArguments = "Positional arguments are not supported" + errStabilityLevel = "StabilityLevel should be passed STABLE, BETA, ALPHA or removed" + errInvalidNewMetricCall = "Invalid new metric call, please ensure code compiles" + errNonStringAttribute = "Non string attribute is not supported" + errBadVariableAttribute = "Metric attribute was not correctly set. Please use only global consts in same file" + errBadImportedVariableAttribute = "Metric attribute was not correctly set. Please use only global consts in correctly impoprted same file" + errFieldNotSupported = "Field %s is not supported" + errBuckets = "Buckets should be set to list of floats, result from function call of prometheus.LinearBuckets or prometheus.ExponentialBuckets" + errObjectives = "Objectives should be set to map of floats to floats" + errDecodeUint32 = "Should decode to uint32" + errDecodeInt64 = "Should decode to int64" + + errLabels = "Labels were not set to list of strings" + errImport = `Importing using "." is not supported` + errExprNotIdent = "expr selector does not refer to type ast.Ident, is type %s" + + errorDecodingString = "can't decode string" + errorDecodingLabels = "can't decode labels" + errorDecodingConstLabels = "can't decode const labels" + errorDecodingStabilityLevel = "can't decode stability level" + errorFindingVariableForBuckets = "couldn't find variable for bucket" + errorFindingVariableForLabels = "couldn't find variable for labels" +) + +type decodeError struct { + msg string + pos token.Pos +} + +func newDecodeErrorf(node ast.Node, format string, a ...interface{}) *decodeError { + return &decodeError{ + msg: fmt.Sprintf(format, a...), + pos: node.Pos(), + } +} + +var _ error = (*decodeError)(nil) + +func (e decodeError) Error() string { + return e.msg +} + +func (e decodeError) errorWithFileInformation(fileset *token.FileSet) error { + position := fileset.Position(e.pos) + return fmt.Errorf("%s:%d:%d: %s", position.Filename, position.Line, position.Column, e.msg) +} diff --git a/tests/stablemetrics/find_stable_metric.go b/tests/stablemetrics/find_stable_metric.go new file mode 100644 index 0000000000..15f3682e5f --- /dev/null +++ b/tests/stablemetrics/find_stable_metric.go @@ -0,0 +1,63 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 main + +import ( + "go/ast" +) + +func findStableMetricDeclaration(tree ast.Node, metricsImportName string) ([]*ast.CallExpr, []error) { + v := stableMetricFinder{ + metricsImportName: metricsImportName, + stableMetricsFunctionCalls: []*ast.CallExpr{}, + errors: []error{}, + } + ast.Walk(&v, tree) + return v.stableMetricsFunctionCalls, v.errors +} + +// Implements visitor pattern for ast.Node that collects all stable metric expressions +type stableMetricFinder struct { + metricsImportName string + currentFunctionCall *ast.CallExpr + stableMetricsFunctionCalls []*ast.CallExpr + errors []error +} + +var _ ast.Visitor = (*stableMetricFinder)(nil) + +func (f *stableMetricFinder) Visit(node ast.Node) (w ast.Visitor) { + switch opts := node.(type) { + case *ast.CallExpr: + f.currentFunctionCall = opts + if se, ok := opts.Fun.(*ast.SelectorExpr); ok { + if se.Sel.Name == "NewFamilyGeneratorWithStabilityV2" { + sl, _ := decodeStabilityLevel(opts.Args[3], f.metricsImportName) + if sl != nil && string(*sl) == "STABLE" { + f.stableMetricsFunctionCalls = append(f.stableMetricsFunctionCalls, opts) + f.currentFunctionCall = nil + } + } + } + default: + if f.currentFunctionCall == nil || node == nil || node.Pos() < f.currentFunctionCall.Rparen { + return f + } + f.currentFunctionCall = nil + } + return f +} diff --git a/tests/stablemetrics/main.go b/tests/stablemetrics/main.go new file mode 100644 index 0000000000..cb0245962c --- /dev/null +++ b/tests/stablemetrics/main.go @@ -0,0 +1,187 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 main + +import ( + "bufio" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strings" + + "gopkg.in/yaml.v2" +) + +const ( + kubeMetricImportPath = `"k8s.io/component-base/metrics"` + // Should equal to final directory name of kubeMetricImportPath + kubeMetricsDefaultImportName = "metrics" +) + +func main() { + + flag.Parse() + if len(flag.Args()) < 1 { + fmt.Fprintf(os.Stderr, "USAGE: %s [...]\n", os.Args[0]) + os.Exit(64) + } + stableMetricNames := map[string]struct{}{} + stableMetrics := []metric{} + errors := []error{} + + addStdin := false + for _, arg := range flag.Args() { + if arg == "-" { + addStdin = true + continue + } + ms, es := searchPathForStableMetrics(arg) + for _, m := range ms { + if _, ok := stableMetricNames[m.Name]; !ok { + stableMetrics = append(stableMetrics, m) + } + stableMetricNames[m.Name] = struct{}{} + } + errors = append(errors, es...) + } + if addStdin { + scanner := bufio.NewScanner(os.Stdin) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + arg := scanner.Text() + ms, es := searchPathForStableMetrics(arg) + stableMetrics = append(stableMetrics, ms...) + errors = append(errors, es...) + } + } + + for _, err := range errors { + fmt.Fprintf(os.Stderr, "%s\n", err) + } + if len(errors) != 0 { + os.Exit(1) + } + if len(stableMetrics) == 0 { + os.Exit(0) + } + for i, m := range stableMetrics { + if m.StabilityLevel == "" { + m.StabilityLevel = "ALPHA" + } + stableMetrics[i] = m + } + sort.Sort(byFQName(stableMetrics)) + data, err := yaml.Marshal(stableMetrics) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + + fmt.Print(string(data)) +} + +func searchPathForStableMetrics(path string) ([]metric, []error) { + metrics := []metric{} + errors := []error{} + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if strings.HasPrefix(path, "vendor") { + return filepath.SkipDir + } + if !strings.HasSuffix(path, ".go") { + return nil + } + ms, es := searchFileForStableMetrics(path, nil) + errors = append(errors, es...) + metrics = append(metrics, ms...) + return nil + }) + if err != nil { + errors = append(errors, err) + } + return metrics, errors +} + +// Pass either only filename of existing file or src including source code in any format and a filename that it comes from +func searchFileForStableMetrics(filename string, src interface{}) ([]metric, []error) { + fileset := token.NewFileSet() + tree, err := parser.ParseFile(fileset, filename, src, parser.AllErrors) + if err != nil { + return []metric{}, []error{err} + } + metricsImportName, err := getLocalNameOfImportedPackage(tree, kubeMetricImportPath, kubeMetricsDefaultImportName) + if err != nil { + return []metric{}, addFileInformationToErrors([]error{err}, fileset) + } + if metricsImportName == "" { + return []metric{}, []error{} + } + variables := globalVariableDeclarations(tree) + stableMetricsFunctionCalls, errors := findStableMetricDeclaration(tree, metricsImportName) + + metrics, es := decodeMetricCalls(stableMetricsFunctionCalls, metricsImportName, variables) + errors = append(errors, es...) + return metrics, addFileInformationToErrors(errors, fileset) +} + +func getLocalNameOfImportedPackage(tree *ast.File, importPath, defaultImportName string) (string, error) { + var importName string + for _, im := range tree.Imports { + if im.Path.Value == importPath { + if im.Name == nil { + importName = defaultImportName + } else { + if im.Name.Name == "." { + return "", newDecodeErrorf(im, errImport) + } + importName = im.Name.Name + } + } + } + return importName, nil +} + +func addFileInformationToErrors(es []error, fileset *token.FileSet) []error { + for i := range es { + if de, ok := es[i].(*decodeError); ok { + es[i] = de.errorWithFileInformation(fileset) + } + } + return es +} + +func globalVariableDeclarations(tree *ast.File) map[string]ast.Expr { + consts := make(map[string]ast.Expr) + for _, d := range tree.Decls { + if gd, ok := d.(*ast.GenDecl); ok && (gd.Tok == token.CONST || gd.Tok == token.VAR) { + for _, spec := range gd.Specs { + if vspec, ok := spec.(*ast.ValueSpec); ok { + for _, name := range vspec.Names { + for _, value := range vspec.Values { + consts[name.Name] = value + } + } + } + } + } + } + return consts +} diff --git a/tests/stablemetrics/metric.go b/tests/stablemetrics/metric.go new file mode 100644 index 0000000000..411a91e70c --- /dev/null +++ b/tests/stablemetrics/metric.go @@ -0,0 +1,57 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 main + +import ( + "k8s.io/component-base/metrics" +) + +type metric struct { + Name string `yaml:"name" json:"name"` + Subsystem string `yaml:"subsystem,omitempty" json:"subsystem,omitempty"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` + Help string `yaml:"help,omitempty" json:"help,omitempty"` + Type string `yaml:"type,omitempty" json:"type,omitempty"` + DeprecatedVersion string `yaml:"deprecatedVersion,omitempty" json:"deprecatedVersion,omitempty"` + StabilityLevel string `yaml:"stabilityLevel,omitempty" json:"stabilityLevel,omitempty"` + Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"` + Buckets []float64 `yaml:"buckets,omitempty" json:"buckets,omitempty"` + Objectives map[float64]float64 `yaml:"objectives,omitempty" json:"objectives,omitempty"` + AgeBuckets uint32 `yaml:"ageBuckets,omitempty" json:"ageBuckets,omitempty"` + BufCap uint32 `yaml:"bufCap,omitempty" json:"bufCap,omitempty"` + MaxAge int64 `yaml:"maxAge,omitempty" json:"maxAge,omitempty"` + ConstLabels map[string]string `yaml:"constLabels,omitempty" json:"constLabels,omitempty"` +} + +func (m metric) buildFQName() string { + return metrics.BuildFQName(m.Namespace, m.Subsystem, m.Name) +} + +type byFQName []metric + +func (ms byFQName) Len() int { return len(ms) } +func (ms byFQName) Less(i, j int) bool { + if ms[i].StabilityLevel < ms[j].StabilityLevel { + return true + } else if ms[i].StabilityLevel > ms[j].StabilityLevel { + return false + } + return ms[i].buildFQName() < ms[j].buildFQName() +} +func (ms byFQName) Swap(i, j int) { + ms[i], ms[j] = ms[j], ms[i] +} diff --git a/tests/stablemetrics/testdata/test-stable-metrics-list.yaml b/tests/stablemetrics/testdata/test-stable-metrics-list.yaml new file mode 100644 index 0000000000..7a38cd3a69 --- /dev/null +++ b/tests/stablemetrics/testdata/test-stable-metrics-list.yaml @@ -0,0 +1,6 @@ +- name: kube_pod_init_container_status_restarts_total + help: The number of restarts for the init container. + type: Counter + stabilityLevel: STABLE + labels: + - container diff --git a/tests/validate-stability.sh b/tests/validate-stability.sh new file mode 100755 index 0000000000..ab5e032f9a --- /dev/null +++ b/tests/validate-stability.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -o errexit + +temp_file=$(mktemp) +metric_file='internal/store/pod.go' + +go run \ + "tests/stablemetrics/main.go" \ + "tests/stablemetrics/decode_metric.go" \ + "tests/stablemetrics/find_stable_metric.go" \ + "tests/stablemetrics/error.go" \ + "tests/stablemetrics/metric.go" \ + -- \ + "${metric_file}" >"${temp_file}" + + +if diff -u "tests/stablemetrics/testdata/test-stable-metrics-list.yaml" "$temp_file"; then + echo -e "PASS metrics stability verification" +fi