From 80e9a4b5d6fd5f7d721899bb4f2288b6d93d0e1c Mon Sep 17 00:00:00 2001 From: Catherine Fang Date: Tue, 2 Jan 2024 12:09:00 -0500 Subject: [PATCH 1/5] Add function NewFamilyGeneratorWithStabilityV2 --- internal/store/pod.go | 4 ++-- pkg/metric_generator/generator.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) 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, From e2dde3b5291ac0084b62e009d5acefa37d0abae3 Mon Sep 17 00:00:00 2001 From: Catherine Fang Date: Tue, 2 Jan 2024 10:40:05 -0500 Subject: [PATCH 2/5] import finding stable metric codes from https://github.com/kubernetes/kubernetes/tree/master/test/instrumentation --- go.mod | 2 +- tests/stablemetrics/decode_metric.go | 838 ++++++++++++++++++++++ tests/stablemetrics/error.go | 72 ++ tests/stablemetrics/find_stable_metric.go | 159 ++++ tests/stablemetrics/main.go | 308 ++++++++ tests/stablemetrics/metric.go | 66 ++ 6 files changed, 1444 insertions(+), 1 deletion(-) create mode 100644 tests/stablemetrics/decode_metric.go create mode 100644 tests/stablemetrics/error.go create mode 100644 tests/stablemetrics/find_stable_metric.go create mode 100644 tests/stablemetrics/main.go create mode 100644 tests/stablemetrics/metric.go 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/tests/stablemetrics/decode_metric.go b/tests/stablemetrics/decode_metric.go new file mode 100644 index 0000000000..1668511491 --- /dev/null +++ b/tests/stablemetrics/decode_metric.go @@ -0,0 +1,838 @@ +/* +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" + "sort" + "strconv" + "strings" + "time" + + "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 + se, ok := fc.Fun.(*ast.SelectorExpr) + if !ok { + // account for timing ratio histogram functions + switch v := fc.Fun.(type) { + case *ast.Ident: + if v.Name == "NewTimingRatioHistogramVec" { + m, err = c.decodeMetricVecForTimingRatioHistogram(fc) + m.Type = timingRatioHistogram + return &m, err + } + } + return nil, newDecodeErrorf(fc, errNotDirectCall) + } + functionName := se.Sel.String() + functionImport, ok := se.X.(*ast.Ident) + if !ok { + return nil, newDecodeErrorf(fc, errNotDirectCall) + } + if functionImport.String() != c.kubeMetricsImportName { + return nil, nil + } + switch functionName { + case "NewCounter", "NewGauge", "NewHistogram", "NewSummary", "NewTimingHistogram", "NewGaugeFunc": + m, err = c.decodeMetric(fc) + case "NewCounterVec", "NewGaugeVec", "NewHistogramVec", "NewSummaryVec", "NewTimingHistogramVec": + m, err = c.decodeMetricVec(fc) + case "Labels", "HandlerOpts", "HandlerFor", "HandlerWithReset": + return nil, nil + case "NewDesc": + m, err = c.decodeDesc(fc) + default: + return &m, newDecodeErrorf(fc, errNotDirectCall) + } + if err != nil { + return &m, err + } + m.Type = getMetricType(functionName) + return &m, nil +} + +func getMetricType(functionName string) string { + switch functionName { + case "NewDesc": + return customType + case "NewCounter", "NewCounterVec": + return counterMetricType + case "NewGauge", "NewGaugeVec", "NewGaugeFunc": + return gaugeMetricType + case "NewHistogram", "NewHistogramVec": + return histogramMetricType + case "NewSummary", "NewSummaryVec": + return summaryMetricType + case "NewTimingHistogram", "NewTimingHistogramVec", "NewTimingRatioHistogramVec": + return timingRatioHistogram + default: + panic("getMetricType expects correct function name") + } +} + +func (c *metricDecoder) decodeMetric(call *ast.CallExpr) (metric, error) { + if len(call.Args) > 2 { + return metric{}, newDecodeErrorf(call, errInvalidNewMetricCall) + } + return c.decodeOpts(call.Args[0]) +} + +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 + labels, err := c.decodeLabels(ce.Args[2]) + if err != nil { + return *m, newDecodeErrorf(ce, errorDecodingLabels) + } + m.Labels = labels + cLabels, err := c.decodeConstLabels(ce.Args[3]) + if err != nil { + return *m, newDecodeErrorf(ce, "can't decode const labels") + } + m.ConstLabels = cLabels + sl, err := decodeStabilityLevel(ce.Args[4], c.kubeMetricsImportName) + if err != nil { + return *m, newDecodeErrorf(ce, "can't decode stability level") + } + if sl != nil { + m.StabilityLevel = string(*sl) + } + deprecatedVersion, err := c.decodeString(ce.Args[5]) + if err != nil { + return *m, newDecodeErrorf(ce, errorDecodingString) + } + if deprecatedVersion != nil { + m.DeprecatedVersion = *deprecatedVersion + } + 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 + case *ast.CallExpr: + firstArg, secondArg, thirdArg, err := c.decodeBuildFQNameArguments(e) + if err != nil { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + se, ok := e.Fun.(*ast.SelectorExpr) + if ok { + functionName := se.Sel.Name + switch functionName { + case "BuildFQName": + n := metrics.BuildFQName(firstArg, secondArg, thirdArg) + return &n, nil + } + } + case *ast.Ident: + variableExpr, found := c.variables[e.Name] + if !found { + return nil, newDecodeErrorf(expr, errBadVariableAttribute) + } + bl, ok := variableExpr.(*ast.BasicLit) + if !ok { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + value, err := stringValue(bl) + if err != nil { + return nil, err + } + return &value, nil + case *ast.SelectorExpr: + s, ok := e.X.(*ast.Ident) + if !ok { + return nil, newDecodeErrorf(expr, errExprNotIdent, e.X) + } + variableExpr, found := c.variables[strings.Join([]string{s.Name, e.Sel.Name}, ".")] + if !found { + return nil, newDecodeErrorf(expr, errBadImportedVariableAttribute) + } + bl, ok := variableExpr.(*ast.BasicLit) + if !ok { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + value, err := stringValue(bl) + if err != nil { + return nil, err + } + return &value, nil + case *ast.BinaryExpr: + var binaryExpr *ast.BinaryExpr + binaryExpr = e + var okay bool + var value string + okay = true + + for okay { + yV, okay := binaryExpr.Y.(*ast.BasicLit) + if !okay { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + yVal, err := stringValue(yV) + if err != nil { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + value = fmt.Sprintf("%s%s", yVal, value) + x, okay := binaryExpr.X.(*ast.BinaryExpr) + if !okay { + // should be basicLit + xV, okay := binaryExpr.X.(*ast.BasicLit) + if !okay { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + xVal, err := stringValue(xV) + if err != nil { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + value = fmt.Sprintf("%s%s", xVal, value) + break + } + binaryExpr = x + } + return &value, nil + } + return nil, newDecodeErrorf(expr, errorDecodingString) +} + +func (c *metricDecoder) decodeMetricVec(call *ast.CallExpr) (metric, error) { + if len(call.Args) != 2 { + return metric{}, newDecodeErrorf(call, errInvalidNewMetricCall) + } + m, err := c.decodeOpts(call.Args[0]) + if err != nil { + return m, err + } + labels, err := c.decodeLabels(call.Args[1]) + if err != nil { + return m, err + } + sort.Strings(labels) + m.Labels = labels + return m, nil +} + +func (c *metricDecoder) decodeMetricVecForTimingRatioHistogram(call *ast.CallExpr) (metric, error) { + m, err := c.decodeOpts(call.Args[0]) + if err != nil { + return m, err + } + labels, err := c.decodeLabelsFromArray(call.Args[1:]) + if err != nil { + return m, err + } + sort.Strings(labels) + m.Labels = labels + return m, nil +} + +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 (c *metricDecoder) decodeOpts(expr ast.Expr) (metric, error) { + m := metric{ + Labels: []string{}, + } + ue, ok := expr.(*ast.UnaryExpr) + if !ok { + return m, newDecodeErrorf(expr, errInvalidNewMetricCall) + } + cl, ok := ue.X.(*ast.CompositeLit) + if !ok { + return m, newDecodeErrorf(expr, errInvalidNewMetricCall) + } + + for _, expr := range cl.Elts { + kv, ok := expr.(*ast.KeyValueExpr) + if !ok { + return m, newDecodeErrorf(expr, errPositionalArguments) + } + key := fmt.Sprintf("%v", kv.Key) + + switch key { + case "Namespace", "Subsystem", "Name", "Help", "DeprecatedVersion": + var value string + var err error + s, err := c.decodeString(kv.Value) + if err != nil { + return m, newDecodeErrorf(expr, err.Error()) + } + value = *s + switch key { + case "Namespace": + m.Namespace = value + case "Subsystem": + m.Subsystem = value + case "Name": + m.Name = value + case "DeprecatedVersion": + m.DeprecatedVersion = value + case "Help": + m.Help = value + } + case "Buckets": + buckets, err := c.decodeBuckets(kv.Value) + if err != nil { + return m, err + } + sort.Float64s(buckets) + m.Buckets = buckets + case "StabilityLevel": + level, err := decodeStabilityLevel(kv.Value, c.kubeMetricsImportName) + if err != nil { + return m, err + } + m.StabilityLevel = string(*level) + case "ConstLabels": + labels, err := c.decodeConstLabels(kv.Value) + if err != nil { + return m, err + } + m.ConstLabels = labels + case "AgeBuckets", "BufCap": + uintVal, err := c.decodeUint32(kv.Value) + if err != nil { + print(key) + return m, err + } + if key == "AgeBuckets" { + m.AgeBuckets = uintVal + } + if key == "BufCap" { + m.BufCap = uintVal + } + + case "Objectives": + obj, err := c.decodeObjectives(kv.Value) + if err != nil { + print(key) + return m, err + } + m.Objectives = obj + case "MaxAge": + int64Val, err := c.decodeInt64(kv.Value) + if err != nil { + return m, err + } + m.MaxAge = int64Val + default: + return m, newDecodeErrorf(expr, errFieldNotSupported, key) + } + } + return m, nil +} + +func stringValue(bl *ast.BasicLit) (string, error) { + if bl.Kind != token.STRING { + return "", newDecodeErrorf(bl, errNonStringAttribute) + } + return strings.Trim(bl.Value, `"`), nil +} + +func (c *metricDecoder) decodeBuckets(expr ast.Expr) ([]float64, error) { + switch v := expr.(type) { + case *ast.Ident: + variableExpr, found := c.variables[v.Name] + if !found { + return nil, newDecodeErrorf(v, "couldn't find variable for bucket") + } + switch v2 := variableExpr.(type) { + case *ast.CompositeLit: + return decodeListOfFloats(v2, v2.Elts) + case *ast.CallExpr: + float64s, err2, done := c.decodeBucketFunctionCall(v2) + if done { + return float64s, err2 + } + default: + return nil, newDecodeErrorf(v, errorFindingVariableForBuckets) + } + + case *ast.CompositeLit: + return decodeListOfFloats(v, v.Elts) + case *ast.SelectorExpr: + variableName := v.Sel.String() + importName, ok := v.X.(*ast.Ident) + if ok && importName.String() == c.kubeMetricsImportName && variableName == "DefBuckets" { + return metrics.DefBuckets, nil + } + case *ast.CallExpr: + float64s, err2, done := c.decodeBucketFunctionCall(v) + if done { + return float64s, err2 + } + } + return nil, newDecodeErrorf(expr, errBuckets) +} + +func (c *metricDecoder) decodeBucketFunctionCall(v *ast.CallExpr) ([]float64, error, bool) { + se, ok := v.Fun.(*ast.SelectorExpr) + if !ok { + // support merged + if ai, ok := v.Fun.(*ast.Ident); ok && ai.Name == "merge" { + merged := []float64{} + for _, arg := range v.Args { + v2, ok := arg.(*ast.CallExpr) + if !ok { + return nil, newDecodeErrorf(v2, errBuckets), true + } + se, ok = v2.Fun.(*ast.SelectorExpr) + if ok { + functionName := se.Sel.String() + functionImport, ok := se.X.(*ast.Ident) + if !ok { + return nil, newDecodeErrorf(v, errBuckets), true + } + if functionImport.String() != c.kubeMetricsImportName { + return nil, newDecodeErrorf(v, errBuckets), true + } + firstArg, secondArg, thirdArg, err := decodeBucketArguments(v2) + if err != nil { + return nil, newDecodeErrorf(v, errBuckets), true + } + switch functionName { + case "LinearBuckets": + merged = append(merged, metrics.LinearBuckets(firstArg, secondArg, thirdArg)...) + case "ExponentialBuckets": + merged = append(merged, metrics.ExponentialBuckets(firstArg, secondArg, thirdArg)...) + case "ExponentialBucketsRange": + merged = append(merged, metrics.ExponentialBuckets(firstArg, secondArg, thirdArg)...) + // merged = append(merged, metrics.ExponentialBucketsRange(firstArg, secondArg, thirdArg)...) + } + } + } + return merged, nil, true + } + return nil, newDecodeErrorf(v, errBuckets), true + } + functionName := se.Sel.String() + functionImport, ok := se.X.(*ast.Ident) + if !ok { + return nil, newDecodeErrorf(v, errBuckets), true + } + if functionImport.String() != c.kubeMetricsImportName { + return nil, newDecodeErrorf(v, errBuckets), true + } + switch functionName { + case "LinearBuckets": + firstArg, secondArg, thirdArg, err := decodeBucketArguments(v) + if err != nil { + return nil, err, true + } + return metrics.LinearBuckets(firstArg, secondArg, thirdArg), nil, true + case "ExponentialBuckets": + firstArg, secondArg, thirdArg, err := decodeBucketArguments(v) + if err != nil { + return nil, err, true + } + return metrics.ExponentialBuckets(firstArg, secondArg, thirdArg), nil, true + case "ExponentialBucketsRange": + firstArg, secondArg, thirdArg, err := decodeBucketArguments(v) + if err != nil { + return nil, err, true + } + return metrics.ExponentialBuckets(firstArg, secondArg, thirdArg), nil, true + // return metrics.ExponentialBucketsRange(firstArg, secondArg, thirdArg), nil, true + case "MergeBuckets": + merged := []float64{} + for _, arg := range v.Args { + switch argExpr := arg.(type) { + case *ast.CompositeLit: + fs, err := decodeListOfFloats(argExpr, argExpr.Elts) + if err != nil { + return nil, newDecodeErrorf(v, errBuckets), true + } + merged = append(merged, fs...) + case *ast.CallExpr: + se, ok = argExpr.Fun.(*ast.SelectorExpr) + if ok { + functionName := se.Sel.String() + functionImport, ok := se.X.(*ast.Ident) + if !ok { + return nil, newDecodeErrorf(v, errBuckets), true + } + if functionImport.String() != c.kubeMetricsImportName { + return nil, newDecodeErrorf(v, errBuckets), true + } + firstArg, secondArg, thirdArg, err := decodeBucketArguments(argExpr) + if err != nil { + return nil, newDecodeErrorf(v, errBuckets), true + } + switch functionName { + case "LinearBuckets": + merged = append(merged, metrics.LinearBuckets(firstArg, secondArg, thirdArg)...) + case "ExponentialBuckets": + merged = append(merged, metrics.LinearBuckets(firstArg, secondArg, thirdArg)...) + } + } + } + } + return merged, nil, true + } + return nil, nil, false +} + +func (c *metricDecoder) decodeObjectives(expr ast.Expr) (map[float64]float64, error) { + switch v := expr.(type) { + case *ast.CompositeLit: + return decodeFloatMap(v.Elts) + case *ast.Ident: + variableExpr, found := c.variables[v.Name] + if !found { + return nil, newDecodeErrorf(expr, errBadVariableAttribute) + } + return decodeFloatMap(variableExpr.(*ast.CompositeLit).Elts) + } + return nil, newDecodeErrorf(expr, errObjectives) +} + +func (c *metricDecoder) decodeUint32(expr ast.Expr) (uint32, error) { + switch v := expr.(type) { + case *ast.BasicLit: + if v.Kind != token.FLOAT && v.Kind != token.INT { + print(v.Kind) + } + value, err := strconv.ParseUint(v.Value, 10, 32) + if err != nil { + return 0, err + } + return uint32(value), nil + case *ast.SelectorExpr: + variableName := v.Sel.String() + importName, ok := v.X.(*ast.Ident) + if ok && importName.String() == c.kubeMetricsImportName { + if variableName == "DefAgeBuckets" { + // hardcode this for now + return metrics.DefAgeBuckets, nil + } + if variableName == "DefBufCap" { + // hardcode this for now + return metrics.DefBufCap, nil + } + } + case *ast.CallExpr: + _, ok := v.Fun.(*ast.SelectorExpr) + if !ok { + return 0, newDecodeErrorf(v, errDecodeUint32) + } + return 0, nil + } + return 0, newDecodeErrorf(expr, errDecodeUint32) +} + +func (c *metricDecoder) decodeInt64(expr ast.Expr) (int64, error) { + switch v := expr.(type) { + case *ast.BasicLit: + if v.Kind != token.FLOAT && v.Kind != token.INT { + print(v.Kind) + } + + value, err := strconv.ParseInt(v.Value, 10, 64) + if err != nil { + return 0, err + } + return value, nil + case *ast.SelectorExpr: + variableName := v.Sel.String() + importName, ok := v.X.(*ast.Ident) + if ok && importName.String() == c.kubeMetricsImportName { + if variableName == "DefMaxAge" { + // hardcode this for now. This is a duration, but we'll output it as + // an int64 representing nanoseconds. + return int64(metrics.DefMaxAge), nil + } + } + case *ast.Ident: + variableExpr, found := c.variables[v.Name] + if found { + be, ok := variableExpr.(*ast.BinaryExpr) + if ok { + i, err2, done := c.extractTimeExpression(be) + if done { + return i, err2 + } + } + } + case *ast.CallExpr: + _, ok := v.Fun.(*ast.SelectorExpr) + if !ok { + return 0, newDecodeErrorf(v, errDecodeInt64) + } + return 0, nil + case *ast.BinaryExpr: + i, err2, done := c.extractTimeExpression(v) + if done { + return i, err2 + } + } + return 0, newDecodeErrorf(expr, errDecodeInt64) +} + +func (c *metricDecoder) extractTimeExpression(v *ast.BinaryExpr) (int64, error, bool) { + x := v.X.(*ast.BasicLit) + if x.Kind != token.FLOAT && x.Kind != token.INT { + print(x.Kind) + } + + xValue, err := strconv.ParseInt(x.Value, 10, 64) + if err != nil { + return 0, err, true + } + + switch y := v.Y.(type) { + case *ast.SelectorExpr: + variableName := y.Sel.String() + importName, ok := y.X.(*ast.Ident) + if ok && importName.String() == "time" { + if variableName == "Hour" { + return xValue * int64(time.Hour), nil, true + } + if variableName == "Minute" { + return xValue * int64(time.Minute), nil, true + } + if variableName == "Second" { + return xValue * int64(time.Second), nil, true + } + } + } + return 0, nil, false +} + +func decodeFloatMap(exprs []ast.Expr) (map[float64]float64, error) { + buckets := map[float64]float64{} + for _, elt := range exprs { + bl, ok := elt.(*ast.KeyValueExpr) + if !ok { + return nil, newDecodeErrorf(bl, errObjectives) + } + keyExpr, ok := bl.Key.(*ast.BasicLit) + if !ok { + return nil, newDecodeErrorf(bl, errObjectives) + } + valueExpr, ok := bl.Value.(*ast.BasicLit) + if !ok { + return nil, newDecodeErrorf(bl, errObjectives) + } + valueForKey, err := strconv.ParseFloat(keyExpr.Value, 64) + if err != nil { + return nil, newDecodeErrorf(bl, errObjectives) + } + valueForValue, err := strconv.ParseFloat(valueExpr.Value, 64) + if err != nil { + return nil, newDecodeErrorf(bl, errObjectives) + } + buckets[valueForKey] = valueForValue + } + return buckets, nil +} + +func decodeListOfFloats(expr ast.Expr, exprs []ast.Expr) ([]float64, error) { + buckets := make([]float64, len(exprs)) + for i, elt := range exprs { + bl, ok := elt.(*ast.BasicLit) + if !ok { + return nil, newDecodeErrorf(expr, errBuckets) + } + if bl.Kind != token.FLOAT && bl.Kind != token.INT { + return nil, newDecodeErrorf(bl, errBuckets) + } + value, err := strconv.ParseFloat(bl.Value, 64) + if err != nil { + return nil, err + } + buckets[i] = value + } + return buckets, nil +} + +func decodeBucketArguments(fc *ast.CallExpr) (float64, float64, int, error) { + if len(fc.Args) != 3 { + return 0, 0, 0, newDecodeErrorf(fc, errBuckets) + } + strArgs := make([]string, len(fc.Args)) + for i, elt := range fc.Args { + bl, ok := elt.(*ast.BasicLit) + if !ok { + return 0, 0, 0, newDecodeErrorf(bl, errBuckets) + } + if bl.Kind != token.FLOAT && bl.Kind != token.INT { + return 0, 0, 0, newDecodeErrorf(bl, errBuckets) + } + strArgs[i] = bl.Value + } + firstArg, err := strconv.ParseFloat(strArgs[0], 64) + if err != nil { + return 0, 0, 0, newDecodeErrorf(fc.Args[0], errBuckets) + } + secondArg, err := strconv.ParseFloat(strArgs[1], 64) + if err != nil { + return 0, 0, 0, newDecodeErrorf(fc.Args[1], errBuckets) + } + thirdArg, err := strconv.ParseInt(strArgs[2], 10, 64) + if err != nil { + return 0, 0, 0, newDecodeErrorf(fc.Args[2], errBuckets) + } + + return firstArg, secondArg, int(thirdArg), nil +} +func (c *metricDecoder) decodeBuildFQNameArguments(fc *ast.CallExpr) (string, string, string, error) { + if len(fc.Args) != 3 { + return "", "", "", newDecodeErrorf(fc, "can't decode fq name args") + } + strArgs := make([]string, len(fc.Args)) + for i, elt := range fc.Args { + s, err := c.decodeString(elt) + if err != nil || s == nil { + return "", "", "", newDecodeErrorf(fc, err.Error()) + } + strArgs[i] = *s + } + return strArgs[0], strArgs[1], strArgs[2], 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 +} + +func (c *metricDecoder) decodeConstLabels(expr ast.Expr) (map[string]string, error) { + retval := map[string]string{} + switch v := expr.(type) { + case *ast.CompositeLit: + for _, e2 := range v.Elts { + kv := e2.(*ast.KeyValueExpr) + key := "" + switch k := kv.Key.(type) { + + case *ast.Ident: + variableExpr, found := c.variables[k.Name] + if !found { + return nil, newDecodeErrorf(expr, errBadVariableAttribute) + } + bl, ok := variableExpr.(*ast.BasicLit) + if !ok { + return nil, newDecodeErrorf(expr, errNonStringAttribute) + } + k2, err := stringValue(bl) + if err != nil { + return nil, err + } + key = k2 + case *ast.BasicLit: + k2, err := stringValue(k) + if err != nil { + return nil, err + } + key = k2 + } + val, err := stringValue(kv.Value.(*ast.BasicLit)) + if err != nil { + return nil, err + } + retval[key] = val + } + } + return retval, 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..a5975ecbb6 --- /dev/null +++ b/tests/stablemetrics/find_stable_metric.go @@ -0,0 +1,159 @@ +/* +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" + + "k8s.io/component-base/metrics" +) + +var metricsOptionStructuresNames = []string{ + "KubeOpts", + "CounterOpts", + "GaugeOpts", + "HistogramOpts", + "SummaryOpts", + "TimingHistogramOpts", +} + +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 contains(v metrics.StabilityLevel, a []metrics.StabilityLevel) bool { + for _, i := range a { + if i == v { + return true + } + } + return false +} + +func (f *stableMetricFinder) Visit(node ast.Node) (w ast.Visitor) { + switch opts := node.(type) { + case *ast.CallExpr: + if se, ok := opts.Fun.(*ast.SelectorExpr); ok { + if se.Sel.Name == "NewDesc" { + sl, _ := decodeStabilityLevel(opts.Args[4], f.metricsImportName) + if sl != nil { + classes := []metrics.StabilityLevel{metrics.STABLE, metrics.BETA} + if ALL_STABILITY_CLASSES { + classes = append(classes, metrics.ALPHA) + } + switch { + case contains(*sl, classes): + f.stableMetricsFunctionCalls = append(f.stableMetricsFunctionCalls, opts) + f.currentFunctionCall = nil + default: + return nil + } + } + } else { + f.currentFunctionCall = opts + } + + } else { + f.currentFunctionCall = opts + } + case *ast.CompositeLit: + se, ok := opts.Type.(*ast.SelectorExpr) + if !ok { + return f + } + if !isMetricOps(se.Sel.Name) { + return f + } + id, ok := se.X.(*ast.Ident) + if !ok { + return f + } + if id.Name != f.metricsImportName { + return f + } + stabilityLevel, err := getStabilityLevel(opts, f.metricsImportName) + if err != nil { + f.errors = append(f.errors, err) + return nil + } + classes := []metrics.StabilityLevel{metrics.STABLE, metrics.BETA} + if ALL_STABILITY_CLASSES { + classes = append(classes, metrics.ALPHA) + } + switch { + case contains(*stabilityLevel, classes): + if f.currentFunctionCall == nil { + f.errors = append(f.errors, newDecodeErrorf(opts, errNotDirectCall)) + return nil + } + f.stableMetricsFunctionCalls = append(f.stableMetricsFunctionCalls, f.currentFunctionCall) + f.currentFunctionCall = nil + default: + return nil + } + default: + if f.currentFunctionCall == nil || node == nil || node.Pos() < f.currentFunctionCall.Rparen { + return f + } + f.currentFunctionCall = nil + } + return f +} + +func isMetricOps(name string) bool { + var found = false + for _, optsName := range metricsOptionStructuresNames { + if name == optsName { + found = true + break + } + } + return found +} + +func getStabilityLevel(opts *ast.CompositeLit, metricsFrameworkImportName string) (*metrics.StabilityLevel, error) { + for _, expr := range opts.Elts { + kv, ok := expr.(*ast.KeyValueExpr) + if !ok { + return nil, newDecodeErrorf(expr, errPositionalArguments) + } + key := fmt.Sprintf("%v", kv.Key) + if key != "StabilityLevel" { + continue + } + return decodeStabilityLevel(kv.Value, metricsFrameworkImportName) + } + stability := metrics.ALPHA + return &stability, nil +} diff --git a/tests/stablemetrics/main.go b/tests/stablemetrics/main.go new file mode 100644 index 0000000000..58c716ff00 --- /dev/null +++ b/tests/stablemetrics/main.go @@ -0,0 +1,308 @@ +/* +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" + + kubeURLRoot = "k8s.io/kubernetes/" +) + +var ( + // env configs + GOROOT string = os.Getenv("GOROOT") + GOOS string = os.Getenv("GOOS") + KUBE_ROOT string = os.Getenv("KUBE_ROOT") + ALL_STABILITY_CLASSES bool +) + +func main() { + + flag.BoolVar(&ALL_STABILITY_CLASSES, "allstabilityclasses", false, "use this flag to enable all stability classes") + 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) + + variables, err = importedGlobalVariableDeclaration(variables, tree.Imports) + if err != nil { + return []metric{}, addFileInformationToErrors([]error{err}, fileset) + } + + // fmt.Println("## tree", 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 +} + +func localImportPath(importExpr string) (string, error) { + // parse directory path + var pathPrefix string + if strings.Contains(importExpr, kubeURLRoot) { + // search k/k local checkout + pathPrefix = KUBE_ROOT + importExpr = strings.Replace(importExpr, kubeURLRoot, "", 1) + } else if strings.Contains(importExpr, "k8s.io/klog/v2") || strings.Contains(importExpr, "k8s.io/util") { + pathPrefix = strings.Join([]string{KUBE_ROOT, "vendor"}, string(os.PathSeparator)) + } else if strings.Contains(importExpr, "k8s.io/") { + // search k/k/staging local checkout + pathPrefix = strings.Join([]string{KUBE_ROOT, "staging", "src"}, string(os.PathSeparator)) + } else if strings.Contains(importExpr, ".") { + // not stdlib -> prefix with GOMODCACHE + // pathPrefix = strings.Join([]string{KUBE_ROOT, "vendor"}, string(os.PathSeparator)) + + // this requires implementing SIV, skip for now + return "", fmt.Errorf("unable to handle general, non STL imports for metric analysis. import path: %s", importExpr) + } else { + // stdlib -> prefix with GOROOT + pathPrefix = strings.Join([]string{GOROOT, "src"}, string(os.PathSeparator)) + } // ToDo: support non go mod + + crossPlatformImportExpr := strings.Replace(importExpr, "/", string(os.PathSeparator), -1) + importDirectory := strings.Join([]string{pathPrefix, strings.Trim(crossPlatformImportExpr, "\"")}, string(os.PathSeparator)) + + return importDirectory, nil +} + +func importedGlobalVariableDeclaration(localVariables map[string]ast.Expr, imports []*ast.ImportSpec) (map[string]ast.Expr, error) { + for _, im := range imports { + // get imported label + var importAlias string + if im.Name == nil { + pathSegments := strings.Split(im.Path.Value, "/") + importAlias = strings.Trim(pathSegments[len(pathSegments)-1], "\"") + } else { + importAlias = im.Name.String() + } + + // find local path on disk for listed import + importDirectory, err := localImportPath(im.Path.Value) + if err != nil { + // uncomment the below log line if you want to start using non k8s/non stl libs for resolving const/var in metric definitions + // fmt.Fprint(os.Stderr, err.Error() + "\n") + continue + } + + files, err := os.ReadDir(importDirectory) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read import path directory %s with error %s, skipping\n", importDirectory, err) + continue + } + + for _, file := range files { + if file.IsDir() { + // do not grab constants from subpackages + continue + } + + if strings.Contains(file.Name(), "_test") { + // do not parse test files + continue + } + + if !strings.HasSuffix(file.Name(), ".go") { + // not a go code file, do not attempt to parse + continue + } + + fileset := token.NewFileSet() + tree, err := parser.ParseFile(fileset, strings.Join([]string{importDirectory, file.Name()}, string(os.PathSeparator)), nil, parser.AllErrors) + if err != nil { + return nil, fmt.Errorf("failed to parse path %s with error %w", im.Path.Value, err) + } + + // pass parsed filepath into globalVariableDeclarations + variables := globalVariableDeclarations(tree) + + // add returned map into supplied map and prepend import label to all keys + for k, v := range variables { + importK := strings.Join([]string{importAlias, k}, ".") + if _, ok := localVariables[importK]; !ok { + localVariables[importK] = v + } else { + // cross-platform file that gets included in the correct OS build via OS build tags + // use whatever matches GOOS + + if strings.Contains(file.Name(), GOOS) { + // assume at some point we will find the correct OS version of this file + // if we are running on an OS that does not have an OS specific file for something then we will include a constant we shouldn't + // TODO: should we include/exclude based on the build tags? + localVariables[importK] = v + } + + } + } + } + + } + + return localVariables, nil +} diff --git a/tests/stablemetrics/metric.go b/tests/stablemetrics/metric.go new file mode 100644 index 0000000000..061596bcae --- /dev/null +++ b/tests/stablemetrics/metric.go @@ -0,0 +1,66 @@ +/* +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" +) + +const ( + counterMetricType = "Counter" + gaugeMetricType = "Gauge" + histogramMetricType = "Histogram" + summaryMetricType = "Summary" + timingRatioHistogram = "TimingRatioHistogram" + customType = "Custom" +) + +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] +} From 90fa42d11a6c33407740b9402a773fe56f50439f Mon Sep 17 00:00:00 2001 From: Catherine Fang Date: Tue, 2 Jan 2024 12:09:00 -0500 Subject: [PATCH 3/5] Adapt code for KSM stable metrics --- tests/stablemetrics/decode_metric.go | 703 +--------------------- tests/stablemetrics/find_stable_metric.go | 108 +--- tests/stablemetrics/main.go | 123 +--- tests/stablemetrics/metric.go | 9 - 4 files changed, 26 insertions(+), 917 deletions(-) diff --git a/tests/stablemetrics/decode_metric.go b/tests/stablemetrics/decode_metric.go index 1668511491..06acfca487 100644 --- a/tests/stablemetrics/decode_metric.go +++ b/tests/stablemetrics/decode_metric.go @@ -17,13 +17,9 @@ limitations under the License. package main import ( - "fmt" "go/ast" "go/token" - "sort" - "strconv" "strings" - "time" "k8s.io/component-base/metrics" ) @@ -56,112 +52,56 @@ type metricDecoder struct { func (c *metricDecoder) decodeNewMetricCall(fc *ast.CallExpr) (*metric, error) { var m metric var err error - se, ok := fc.Fun.(*ast.SelectorExpr) - if !ok { - // account for timing ratio histogram functions - switch v := fc.Fun.(type) { - case *ast.Ident: - if v.Name == "NewTimingRatioHistogramVec" { - m, err = c.decodeMetricVecForTimingRatioHistogram(fc) - m.Type = timingRatioHistogram - return &m, err - } - } - return nil, newDecodeErrorf(fc, errNotDirectCall) - } - functionName := se.Sel.String() - functionImport, ok := se.X.(*ast.Ident) + + _, ok := fc.Fun.(*ast.SelectorExpr) if !ok { return nil, newDecodeErrorf(fc, errNotDirectCall) } - if functionImport.String() != c.kubeMetricsImportName { - return nil, nil - } - switch functionName { - case "NewCounter", "NewGauge", "NewHistogram", "NewSummary", "NewTimingHistogram", "NewGaugeFunc": - m, err = c.decodeMetric(fc) - case "NewCounterVec", "NewGaugeVec", "NewHistogramVec", "NewSummaryVec", "NewTimingHistogramVec": - m, err = c.decodeMetricVec(fc) - case "Labels", "HandlerOpts", "HandlerFor", "HandlerWithReset": - return nil, nil - case "NewDesc": - m, err = c.decodeDesc(fc) - default: - return &m, newDecodeErrorf(fc, errNotDirectCall) - } - if err != nil { - return &m, err - } - m.Type = getMetricType(functionName) - return &m, nil -} - -func getMetricType(functionName string) string { - switch functionName { - case "NewDesc": - return customType - case "NewCounter", "NewCounterVec": - return counterMetricType - case "NewGauge", "NewGaugeVec", "NewGaugeFunc": - return gaugeMetricType - case "NewHistogram", "NewHistogramVec": - return histogramMetricType - case "NewSummary", "NewSummaryVec": - return summaryMetricType - case "NewTimingHistogram", "NewTimingHistogramVec", "NewTimingRatioHistogramVec": - return timingRatioHistogram - default: - panic("getMetricType expects correct function name") - } -} - -func (c *metricDecoder) decodeMetric(call *ast.CallExpr) (metric, error) { - if len(call.Args) > 2 { - return metric{}, newDecodeErrorf(call, errInvalidNewMetricCall) - } - return c.decodeOpts(call.Args[0]) + 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 - labels, err := c.decodeLabels(ce.Args[2]) - if err != nil { - return *m, newDecodeErrorf(ce, errorDecodingLabels) - } - m.Labels = labels - cLabels, err := c.decodeConstLabels(ce.Args[3]) + + metricType, err := decodeStabilityLevel(ce.Args[2], "metric") if err != nil { - return *m, newDecodeErrorf(ce, "can't decode const labels") + return *m, newDecodeErrorf(ce, errorDecodingString) } - m.ConstLabels = cLabels - sl, err := decodeStabilityLevel(ce.Args[4], c.kubeMetricsImportName) + 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) } - deprecatedVersion, err := c.decodeString(ce.Args[5]) + + labels, err := c.decodeLabels(ce.Args[5]) if err != nil { - return *m, newDecodeErrorf(ce, errorDecodingString) - } - if deprecatedVersion != nil { - m.DeprecatedVersion = *deprecatedVersion + 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) @@ -169,121 +109,10 @@ func (c *metricDecoder) decodeString(expr ast.Expr) (*string, error) { return nil, err } return &value, nil - case *ast.CallExpr: - firstArg, secondArg, thirdArg, err := c.decodeBuildFQNameArguments(e) - if err != nil { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - se, ok := e.Fun.(*ast.SelectorExpr) - if ok { - functionName := se.Sel.Name - switch functionName { - case "BuildFQName": - n := metrics.BuildFQName(firstArg, secondArg, thirdArg) - return &n, nil - } - } - case *ast.Ident: - variableExpr, found := c.variables[e.Name] - if !found { - return nil, newDecodeErrorf(expr, errBadVariableAttribute) - } - bl, ok := variableExpr.(*ast.BasicLit) - if !ok { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - value, err := stringValue(bl) - if err != nil { - return nil, err - } - return &value, nil - case *ast.SelectorExpr: - s, ok := e.X.(*ast.Ident) - if !ok { - return nil, newDecodeErrorf(expr, errExprNotIdent, e.X) - } - variableExpr, found := c.variables[strings.Join([]string{s.Name, e.Sel.Name}, ".")] - if !found { - return nil, newDecodeErrorf(expr, errBadImportedVariableAttribute) - } - bl, ok := variableExpr.(*ast.BasicLit) - if !ok { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - value, err := stringValue(bl) - if err != nil { - return nil, err - } - return &value, nil - case *ast.BinaryExpr: - var binaryExpr *ast.BinaryExpr - binaryExpr = e - var okay bool - var value string - okay = true - - for okay { - yV, okay := binaryExpr.Y.(*ast.BasicLit) - if !okay { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - yVal, err := stringValue(yV) - if err != nil { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - value = fmt.Sprintf("%s%s", yVal, value) - x, okay := binaryExpr.X.(*ast.BinaryExpr) - if !okay { - // should be basicLit - xV, okay := binaryExpr.X.(*ast.BasicLit) - if !okay { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - xVal, err := stringValue(xV) - if err != nil { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - value = fmt.Sprintf("%s%s", xVal, value) - break - } - binaryExpr = x - } - return &value, nil } return nil, newDecodeErrorf(expr, errorDecodingString) } -func (c *metricDecoder) decodeMetricVec(call *ast.CallExpr) (metric, error) { - if len(call.Args) != 2 { - return metric{}, newDecodeErrorf(call, errInvalidNewMetricCall) - } - m, err := c.decodeOpts(call.Args[0]) - if err != nil { - return m, err - } - labels, err := c.decodeLabels(call.Args[1]) - if err != nil { - return m, err - } - sort.Strings(labels) - m.Labels = labels - return m, nil -} - -func (c *metricDecoder) decodeMetricVecForTimingRatioHistogram(call *ast.CallExpr) (metric, error) { - m, err := c.decodeOpts(call.Args[0]) - if err != nil { - return m, err - } - labels, err := c.decodeLabelsFromArray(call.Args[1:]) - if err != nil { - return m, err - } - sort.Strings(labels) - m.Labels = labels - return m, nil -} - func (c *metricDecoder) decodeLabelsFromArray(exprs []ast.Expr) ([]string, error) { retval := []string{} for _, e := range exprs { @@ -319,99 +148,6 @@ func (c *metricDecoder) decodeLabels(expr ast.Expr) ([]string, error) { return c.decodeLabelsFromArray(cl.Elts) } -func (c *metricDecoder) decodeOpts(expr ast.Expr) (metric, error) { - m := metric{ - Labels: []string{}, - } - ue, ok := expr.(*ast.UnaryExpr) - if !ok { - return m, newDecodeErrorf(expr, errInvalidNewMetricCall) - } - cl, ok := ue.X.(*ast.CompositeLit) - if !ok { - return m, newDecodeErrorf(expr, errInvalidNewMetricCall) - } - - for _, expr := range cl.Elts { - kv, ok := expr.(*ast.KeyValueExpr) - if !ok { - return m, newDecodeErrorf(expr, errPositionalArguments) - } - key := fmt.Sprintf("%v", kv.Key) - - switch key { - case "Namespace", "Subsystem", "Name", "Help", "DeprecatedVersion": - var value string - var err error - s, err := c.decodeString(kv.Value) - if err != nil { - return m, newDecodeErrorf(expr, err.Error()) - } - value = *s - switch key { - case "Namespace": - m.Namespace = value - case "Subsystem": - m.Subsystem = value - case "Name": - m.Name = value - case "DeprecatedVersion": - m.DeprecatedVersion = value - case "Help": - m.Help = value - } - case "Buckets": - buckets, err := c.decodeBuckets(kv.Value) - if err != nil { - return m, err - } - sort.Float64s(buckets) - m.Buckets = buckets - case "StabilityLevel": - level, err := decodeStabilityLevel(kv.Value, c.kubeMetricsImportName) - if err != nil { - return m, err - } - m.StabilityLevel = string(*level) - case "ConstLabels": - labels, err := c.decodeConstLabels(kv.Value) - if err != nil { - return m, err - } - m.ConstLabels = labels - case "AgeBuckets", "BufCap": - uintVal, err := c.decodeUint32(kv.Value) - if err != nil { - print(key) - return m, err - } - if key == "AgeBuckets" { - m.AgeBuckets = uintVal - } - if key == "BufCap" { - m.BufCap = uintVal - } - - case "Objectives": - obj, err := c.decodeObjectives(kv.Value) - if err != nil { - print(key) - return m, err - } - m.Objectives = obj - case "MaxAge": - int64Val, err := c.decodeInt64(kv.Value) - if err != nil { - return m, err - } - m.MaxAge = int64Val - default: - return m, newDecodeErrorf(expr, errFieldNotSupported, key) - } - } - return m, nil -} - func stringValue(bl *ast.BasicLit) (string, error) { if bl.Kind != token.STRING { return "", newDecodeErrorf(bl, errNonStringAttribute) @@ -419,367 +155,6 @@ func stringValue(bl *ast.BasicLit) (string, error) { return strings.Trim(bl.Value, `"`), nil } -func (c *metricDecoder) decodeBuckets(expr ast.Expr) ([]float64, error) { - switch v := expr.(type) { - case *ast.Ident: - variableExpr, found := c.variables[v.Name] - if !found { - return nil, newDecodeErrorf(v, "couldn't find variable for bucket") - } - switch v2 := variableExpr.(type) { - case *ast.CompositeLit: - return decodeListOfFloats(v2, v2.Elts) - case *ast.CallExpr: - float64s, err2, done := c.decodeBucketFunctionCall(v2) - if done { - return float64s, err2 - } - default: - return nil, newDecodeErrorf(v, errorFindingVariableForBuckets) - } - - case *ast.CompositeLit: - return decodeListOfFloats(v, v.Elts) - case *ast.SelectorExpr: - variableName := v.Sel.String() - importName, ok := v.X.(*ast.Ident) - if ok && importName.String() == c.kubeMetricsImportName && variableName == "DefBuckets" { - return metrics.DefBuckets, nil - } - case *ast.CallExpr: - float64s, err2, done := c.decodeBucketFunctionCall(v) - if done { - return float64s, err2 - } - } - return nil, newDecodeErrorf(expr, errBuckets) -} - -func (c *metricDecoder) decodeBucketFunctionCall(v *ast.CallExpr) ([]float64, error, bool) { - se, ok := v.Fun.(*ast.SelectorExpr) - if !ok { - // support merged - if ai, ok := v.Fun.(*ast.Ident); ok && ai.Name == "merge" { - merged := []float64{} - for _, arg := range v.Args { - v2, ok := arg.(*ast.CallExpr) - if !ok { - return nil, newDecodeErrorf(v2, errBuckets), true - } - se, ok = v2.Fun.(*ast.SelectorExpr) - if ok { - functionName := se.Sel.String() - functionImport, ok := se.X.(*ast.Ident) - if !ok { - return nil, newDecodeErrorf(v, errBuckets), true - } - if functionImport.String() != c.kubeMetricsImportName { - return nil, newDecodeErrorf(v, errBuckets), true - } - firstArg, secondArg, thirdArg, err := decodeBucketArguments(v2) - if err != nil { - return nil, newDecodeErrorf(v, errBuckets), true - } - switch functionName { - case "LinearBuckets": - merged = append(merged, metrics.LinearBuckets(firstArg, secondArg, thirdArg)...) - case "ExponentialBuckets": - merged = append(merged, metrics.ExponentialBuckets(firstArg, secondArg, thirdArg)...) - case "ExponentialBucketsRange": - merged = append(merged, metrics.ExponentialBuckets(firstArg, secondArg, thirdArg)...) - // merged = append(merged, metrics.ExponentialBucketsRange(firstArg, secondArg, thirdArg)...) - } - } - } - return merged, nil, true - } - return nil, newDecodeErrorf(v, errBuckets), true - } - functionName := se.Sel.String() - functionImport, ok := se.X.(*ast.Ident) - if !ok { - return nil, newDecodeErrorf(v, errBuckets), true - } - if functionImport.String() != c.kubeMetricsImportName { - return nil, newDecodeErrorf(v, errBuckets), true - } - switch functionName { - case "LinearBuckets": - firstArg, secondArg, thirdArg, err := decodeBucketArguments(v) - if err != nil { - return nil, err, true - } - return metrics.LinearBuckets(firstArg, secondArg, thirdArg), nil, true - case "ExponentialBuckets": - firstArg, secondArg, thirdArg, err := decodeBucketArguments(v) - if err != nil { - return nil, err, true - } - return metrics.ExponentialBuckets(firstArg, secondArg, thirdArg), nil, true - case "ExponentialBucketsRange": - firstArg, secondArg, thirdArg, err := decodeBucketArguments(v) - if err != nil { - return nil, err, true - } - return metrics.ExponentialBuckets(firstArg, secondArg, thirdArg), nil, true - // return metrics.ExponentialBucketsRange(firstArg, secondArg, thirdArg), nil, true - case "MergeBuckets": - merged := []float64{} - for _, arg := range v.Args { - switch argExpr := arg.(type) { - case *ast.CompositeLit: - fs, err := decodeListOfFloats(argExpr, argExpr.Elts) - if err != nil { - return nil, newDecodeErrorf(v, errBuckets), true - } - merged = append(merged, fs...) - case *ast.CallExpr: - se, ok = argExpr.Fun.(*ast.SelectorExpr) - if ok { - functionName := se.Sel.String() - functionImport, ok := se.X.(*ast.Ident) - if !ok { - return nil, newDecodeErrorf(v, errBuckets), true - } - if functionImport.String() != c.kubeMetricsImportName { - return nil, newDecodeErrorf(v, errBuckets), true - } - firstArg, secondArg, thirdArg, err := decodeBucketArguments(argExpr) - if err != nil { - return nil, newDecodeErrorf(v, errBuckets), true - } - switch functionName { - case "LinearBuckets": - merged = append(merged, metrics.LinearBuckets(firstArg, secondArg, thirdArg)...) - case "ExponentialBuckets": - merged = append(merged, metrics.LinearBuckets(firstArg, secondArg, thirdArg)...) - } - } - } - } - return merged, nil, true - } - return nil, nil, false -} - -func (c *metricDecoder) decodeObjectives(expr ast.Expr) (map[float64]float64, error) { - switch v := expr.(type) { - case *ast.CompositeLit: - return decodeFloatMap(v.Elts) - case *ast.Ident: - variableExpr, found := c.variables[v.Name] - if !found { - return nil, newDecodeErrorf(expr, errBadVariableAttribute) - } - return decodeFloatMap(variableExpr.(*ast.CompositeLit).Elts) - } - return nil, newDecodeErrorf(expr, errObjectives) -} - -func (c *metricDecoder) decodeUint32(expr ast.Expr) (uint32, error) { - switch v := expr.(type) { - case *ast.BasicLit: - if v.Kind != token.FLOAT && v.Kind != token.INT { - print(v.Kind) - } - value, err := strconv.ParseUint(v.Value, 10, 32) - if err != nil { - return 0, err - } - return uint32(value), nil - case *ast.SelectorExpr: - variableName := v.Sel.String() - importName, ok := v.X.(*ast.Ident) - if ok && importName.String() == c.kubeMetricsImportName { - if variableName == "DefAgeBuckets" { - // hardcode this for now - return metrics.DefAgeBuckets, nil - } - if variableName == "DefBufCap" { - // hardcode this for now - return metrics.DefBufCap, nil - } - } - case *ast.CallExpr: - _, ok := v.Fun.(*ast.SelectorExpr) - if !ok { - return 0, newDecodeErrorf(v, errDecodeUint32) - } - return 0, nil - } - return 0, newDecodeErrorf(expr, errDecodeUint32) -} - -func (c *metricDecoder) decodeInt64(expr ast.Expr) (int64, error) { - switch v := expr.(type) { - case *ast.BasicLit: - if v.Kind != token.FLOAT && v.Kind != token.INT { - print(v.Kind) - } - - value, err := strconv.ParseInt(v.Value, 10, 64) - if err != nil { - return 0, err - } - return value, nil - case *ast.SelectorExpr: - variableName := v.Sel.String() - importName, ok := v.X.(*ast.Ident) - if ok && importName.String() == c.kubeMetricsImportName { - if variableName == "DefMaxAge" { - // hardcode this for now. This is a duration, but we'll output it as - // an int64 representing nanoseconds. - return int64(metrics.DefMaxAge), nil - } - } - case *ast.Ident: - variableExpr, found := c.variables[v.Name] - if found { - be, ok := variableExpr.(*ast.BinaryExpr) - if ok { - i, err2, done := c.extractTimeExpression(be) - if done { - return i, err2 - } - } - } - case *ast.CallExpr: - _, ok := v.Fun.(*ast.SelectorExpr) - if !ok { - return 0, newDecodeErrorf(v, errDecodeInt64) - } - return 0, nil - case *ast.BinaryExpr: - i, err2, done := c.extractTimeExpression(v) - if done { - return i, err2 - } - } - return 0, newDecodeErrorf(expr, errDecodeInt64) -} - -func (c *metricDecoder) extractTimeExpression(v *ast.BinaryExpr) (int64, error, bool) { - x := v.X.(*ast.BasicLit) - if x.Kind != token.FLOAT && x.Kind != token.INT { - print(x.Kind) - } - - xValue, err := strconv.ParseInt(x.Value, 10, 64) - if err != nil { - return 0, err, true - } - - switch y := v.Y.(type) { - case *ast.SelectorExpr: - variableName := y.Sel.String() - importName, ok := y.X.(*ast.Ident) - if ok && importName.String() == "time" { - if variableName == "Hour" { - return xValue * int64(time.Hour), nil, true - } - if variableName == "Minute" { - return xValue * int64(time.Minute), nil, true - } - if variableName == "Second" { - return xValue * int64(time.Second), nil, true - } - } - } - return 0, nil, false -} - -func decodeFloatMap(exprs []ast.Expr) (map[float64]float64, error) { - buckets := map[float64]float64{} - for _, elt := range exprs { - bl, ok := elt.(*ast.KeyValueExpr) - if !ok { - return nil, newDecodeErrorf(bl, errObjectives) - } - keyExpr, ok := bl.Key.(*ast.BasicLit) - if !ok { - return nil, newDecodeErrorf(bl, errObjectives) - } - valueExpr, ok := bl.Value.(*ast.BasicLit) - if !ok { - return nil, newDecodeErrorf(bl, errObjectives) - } - valueForKey, err := strconv.ParseFloat(keyExpr.Value, 64) - if err != nil { - return nil, newDecodeErrorf(bl, errObjectives) - } - valueForValue, err := strconv.ParseFloat(valueExpr.Value, 64) - if err != nil { - return nil, newDecodeErrorf(bl, errObjectives) - } - buckets[valueForKey] = valueForValue - } - return buckets, nil -} - -func decodeListOfFloats(expr ast.Expr, exprs []ast.Expr) ([]float64, error) { - buckets := make([]float64, len(exprs)) - for i, elt := range exprs { - bl, ok := elt.(*ast.BasicLit) - if !ok { - return nil, newDecodeErrorf(expr, errBuckets) - } - if bl.Kind != token.FLOAT && bl.Kind != token.INT { - return nil, newDecodeErrorf(bl, errBuckets) - } - value, err := strconv.ParseFloat(bl.Value, 64) - if err != nil { - return nil, err - } - buckets[i] = value - } - return buckets, nil -} - -func decodeBucketArguments(fc *ast.CallExpr) (float64, float64, int, error) { - if len(fc.Args) != 3 { - return 0, 0, 0, newDecodeErrorf(fc, errBuckets) - } - strArgs := make([]string, len(fc.Args)) - for i, elt := range fc.Args { - bl, ok := elt.(*ast.BasicLit) - if !ok { - return 0, 0, 0, newDecodeErrorf(bl, errBuckets) - } - if bl.Kind != token.FLOAT && bl.Kind != token.INT { - return 0, 0, 0, newDecodeErrorf(bl, errBuckets) - } - strArgs[i] = bl.Value - } - firstArg, err := strconv.ParseFloat(strArgs[0], 64) - if err != nil { - return 0, 0, 0, newDecodeErrorf(fc.Args[0], errBuckets) - } - secondArg, err := strconv.ParseFloat(strArgs[1], 64) - if err != nil { - return 0, 0, 0, newDecodeErrorf(fc.Args[1], errBuckets) - } - thirdArg, err := strconv.ParseInt(strArgs[2], 10, 64) - if err != nil { - return 0, 0, 0, newDecodeErrorf(fc.Args[2], errBuckets) - } - - return firstArg, secondArg, int(thirdArg), nil -} -func (c *metricDecoder) decodeBuildFQNameArguments(fc *ast.CallExpr) (string, string, string, error) { - if len(fc.Args) != 3 { - return "", "", "", newDecodeErrorf(fc, "can't decode fq name args") - } - strArgs := make([]string, len(fc.Args)) - for i, elt := range fc.Args { - s, err := c.decodeString(elt) - if err != nil || s == nil { - return "", "", "", newDecodeErrorf(fc, err.Error()) - } - strArgs[i] = *s - } - return strArgs[0], strArgs[1], strArgs[2], nil -} - func decodeStabilityLevel(expr ast.Expr, metricsFrameworkImportName string) (*metrics.StabilityLevel, error) { se, ok := expr.(*ast.SelectorExpr) if !ok { @@ -796,43 +171,3 @@ func decodeStabilityLevel(expr ast.Expr, metricsFrameworkImportName string) (*me stability := metrics.StabilityLevel(se.Sel.Name) return &stability, nil } - -func (c *metricDecoder) decodeConstLabels(expr ast.Expr) (map[string]string, error) { - retval := map[string]string{} - switch v := expr.(type) { - case *ast.CompositeLit: - for _, e2 := range v.Elts { - kv := e2.(*ast.KeyValueExpr) - key := "" - switch k := kv.Key.(type) { - - case *ast.Ident: - variableExpr, found := c.variables[k.Name] - if !found { - return nil, newDecodeErrorf(expr, errBadVariableAttribute) - } - bl, ok := variableExpr.(*ast.BasicLit) - if !ok { - return nil, newDecodeErrorf(expr, errNonStringAttribute) - } - k2, err := stringValue(bl) - if err != nil { - return nil, err - } - key = k2 - case *ast.BasicLit: - k2, err := stringValue(k) - if err != nil { - return nil, err - } - key = k2 - } - val, err := stringValue(kv.Value.(*ast.BasicLit)) - if err != nil { - return nil, err - } - retval[key] = val - } - } - return retval, nil -} diff --git a/tests/stablemetrics/find_stable_metric.go b/tests/stablemetrics/find_stable_metric.go index a5975ecbb6..15f3682e5f 100644 --- a/tests/stablemetrics/find_stable_metric.go +++ b/tests/stablemetrics/find_stable_metric.go @@ -17,21 +17,9 @@ limitations under the License. package main import ( - "fmt" "go/ast" - - "k8s.io/component-base/metrics" ) -var metricsOptionStructuresNames = []string{ - "KubeOpts", - "CounterOpts", - "GaugeOpts", - "HistogramOpts", - "SummaryOpts", - "TimingHistogramOpts", -} - func findStableMetricDeclaration(tree ast.Node, metricsImportName string) ([]*ast.CallExpr, []error) { v := stableMetricFinder{ metricsImportName: metricsImportName, @@ -52,75 +40,18 @@ type stableMetricFinder struct { var _ ast.Visitor = (*stableMetricFinder)(nil) -func contains(v metrics.StabilityLevel, a []metrics.StabilityLevel) bool { - for _, i := range a { - if i == v { - return true - } - } - return false -} - 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 == "NewDesc" { - sl, _ := decodeStabilityLevel(opts.Args[4], f.metricsImportName) - if sl != nil { - classes := []metrics.StabilityLevel{metrics.STABLE, metrics.BETA} - if ALL_STABILITY_CLASSES { - classes = append(classes, metrics.ALPHA) - } - switch { - case contains(*sl, classes): - f.stableMetricsFunctionCalls = append(f.stableMetricsFunctionCalls, opts) - f.currentFunctionCall = nil - default: - return nil - } + 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 } - } else { - f.currentFunctionCall = opts } - - } else { - f.currentFunctionCall = opts - } - case *ast.CompositeLit: - se, ok := opts.Type.(*ast.SelectorExpr) - if !ok { - return f - } - if !isMetricOps(se.Sel.Name) { - return f - } - id, ok := se.X.(*ast.Ident) - if !ok { - return f - } - if id.Name != f.metricsImportName { - return f - } - stabilityLevel, err := getStabilityLevel(opts, f.metricsImportName) - if err != nil { - f.errors = append(f.errors, err) - return nil - } - classes := []metrics.StabilityLevel{metrics.STABLE, metrics.BETA} - if ALL_STABILITY_CLASSES { - classes = append(classes, metrics.ALPHA) - } - switch { - case contains(*stabilityLevel, classes): - if f.currentFunctionCall == nil { - f.errors = append(f.errors, newDecodeErrorf(opts, errNotDirectCall)) - return nil - } - f.stableMetricsFunctionCalls = append(f.stableMetricsFunctionCalls, f.currentFunctionCall) - f.currentFunctionCall = nil - default: - return nil } default: if f.currentFunctionCall == nil || node == nil || node.Pos() < f.currentFunctionCall.Rparen { @@ -130,30 +61,3 @@ func (f *stableMetricFinder) Visit(node ast.Node) (w ast.Visitor) { } return f } - -func isMetricOps(name string) bool { - var found = false - for _, optsName := range metricsOptionStructuresNames { - if name == optsName { - found = true - break - } - } - return found -} - -func getStabilityLevel(opts *ast.CompositeLit, metricsFrameworkImportName string) (*metrics.StabilityLevel, error) { - for _, expr := range opts.Elts { - kv, ok := expr.(*ast.KeyValueExpr) - if !ok { - return nil, newDecodeErrorf(expr, errPositionalArguments) - } - key := fmt.Sprintf("%v", kv.Key) - if key != "StabilityLevel" { - continue - } - return decodeStabilityLevel(kv.Value, metricsFrameworkImportName) - } - stability := metrics.ALPHA - return &stability, nil -} diff --git a/tests/stablemetrics/main.go b/tests/stablemetrics/main.go index 58c716ff00..cb0245962c 100644 --- a/tests/stablemetrics/main.go +++ b/tests/stablemetrics/main.go @@ -35,21 +35,10 @@ const ( kubeMetricImportPath = `"k8s.io/component-base/metrics"` // Should equal to final directory name of kubeMetricImportPath kubeMetricsDefaultImportName = "metrics" - - kubeURLRoot = "k8s.io/kubernetes/" -) - -var ( - // env configs - GOROOT string = os.Getenv("GOROOT") - GOOS string = os.Getenv("GOOS") - KUBE_ROOT string = os.Getenv("KUBE_ROOT") - ALL_STABILITY_CLASSES bool ) func main() { - flag.BoolVar(&ALL_STABILITY_CLASSES, "allstabilityclasses", false, "use this flag to enable all stability classes") flag.Parse() if len(flag.Args()) < 1 { fmt.Fprintf(os.Stderr, "USAGE: %s [...]\n", os.Args[0]) @@ -146,14 +135,8 @@ func searchFileForStableMetrics(filename string, src interface{}) ([]metric, []e return []metric{}, []error{} } variables := globalVariableDeclarations(tree) - - variables, err = importedGlobalVariableDeclaration(variables, tree.Imports) - if err != nil { - return []metric{}, addFileInformationToErrors([]error{err}, fileset) - } - - // fmt.Println("## tree", tree) stableMetricsFunctionCalls, errors := findStableMetricDeclaration(tree, metricsImportName) + metrics, es := decodeMetricCalls(stableMetricsFunctionCalls, metricsImportName, variables) errors = append(errors, es...) return metrics, addFileInformationToErrors(errors, fileset) @@ -202,107 +185,3 @@ func globalVariableDeclarations(tree *ast.File) map[string]ast.Expr { } return consts } - -func localImportPath(importExpr string) (string, error) { - // parse directory path - var pathPrefix string - if strings.Contains(importExpr, kubeURLRoot) { - // search k/k local checkout - pathPrefix = KUBE_ROOT - importExpr = strings.Replace(importExpr, kubeURLRoot, "", 1) - } else if strings.Contains(importExpr, "k8s.io/klog/v2") || strings.Contains(importExpr, "k8s.io/util") { - pathPrefix = strings.Join([]string{KUBE_ROOT, "vendor"}, string(os.PathSeparator)) - } else if strings.Contains(importExpr, "k8s.io/") { - // search k/k/staging local checkout - pathPrefix = strings.Join([]string{KUBE_ROOT, "staging", "src"}, string(os.PathSeparator)) - } else if strings.Contains(importExpr, ".") { - // not stdlib -> prefix with GOMODCACHE - // pathPrefix = strings.Join([]string{KUBE_ROOT, "vendor"}, string(os.PathSeparator)) - - // this requires implementing SIV, skip for now - return "", fmt.Errorf("unable to handle general, non STL imports for metric analysis. import path: %s", importExpr) - } else { - // stdlib -> prefix with GOROOT - pathPrefix = strings.Join([]string{GOROOT, "src"}, string(os.PathSeparator)) - } // ToDo: support non go mod - - crossPlatformImportExpr := strings.Replace(importExpr, "/", string(os.PathSeparator), -1) - importDirectory := strings.Join([]string{pathPrefix, strings.Trim(crossPlatformImportExpr, "\"")}, string(os.PathSeparator)) - - return importDirectory, nil -} - -func importedGlobalVariableDeclaration(localVariables map[string]ast.Expr, imports []*ast.ImportSpec) (map[string]ast.Expr, error) { - for _, im := range imports { - // get imported label - var importAlias string - if im.Name == nil { - pathSegments := strings.Split(im.Path.Value, "/") - importAlias = strings.Trim(pathSegments[len(pathSegments)-1], "\"") - } else { - importAlias = im.Name.String() - } - - // find local path on disk for listed import - importDirectory, err := localImportPath(im.Path.Value) - if err != nil { - // uncomment the below log line if you want to start using non k8s/non stl libs for resolving const/var in metric definitions - // fmt.Fprint(os.Stderr, err.Error() + "\n") - continue - } - - files, err := os.ReadDir(importDirectory) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to read import path directory %s with error %s, skipping\n", importDirectory, err) - continue - } - - for _, file := range files { - if file.IsDir() { - // do not grab constants from subpackages - continue - } - - if strings.Contains(file.Name(), "_test") { - // do not parse test files - continue - } - - if !strings.HasSuffix(file.Name(), ".go") { - // not a go code file, do not attempt to parse - continue - } - - fileset := token.NewFileSet() - tree, err := parser.ParseFile(fileset, strings.Join([]string{importDirectory, file.Name()}, string(os.PathSeparator)), nil, parser.AllErrors) - if err != nil { - return nil, fmt.Errorf("failed to parse path %s with error %w", im.Path.Value, err) - } - - // pass parsed filepath into globalVariableDeclarations - variables := globalVariableDeclarations(tree) - - // add returned map into supplied map and prepend import label to all keys - for k, v := range variables { - importK := strings.Join([]string{importAlias, k}, ".") - if _, ok := localVariables[importK]; !ok { - localVariables[importK] = v - } else { - // cross-platform file that gets included in the correct OS build via OS build tags - // use whatever matches GOOS - - if strings.Contains(file.Name(), GOOS) { - // assume at some point we will find the correct OS version of this file - // if we are running on an OS that does not have an OS specific file for something then we will include a constant we shouldn't - // TODO: should we include/exclude based on the build tags? - localVariables[importK] = v - } - - } - } - } - - } - - return localVariables, nil -} diff --git a/tests/stablemetrics/metric.go b/tests/stablemetrics/metric.go index 061596bcae..411a91e70c 100644 --- a/tests/stablemetrics/metric.go +++ b/tests/stablemetrics/metric.go @@ -20,15 +20,6 @@ import ( "k8s.io/component-base/metrics" ) -const ( - counterMetricType = "Counter" - gaugeMetricType = "Gauge" - histogramMetricType = "Histogram" - summaryMetricType = "Summary" - timingRatioHistogram = "TimingRatioHistogram" - customType = "Custom" -) - type metric struct { Name string `yaml:"name" json:"name"` Subsystem string `yaml:"subsystem,omitempty" json:"subsystem,omitempty"` From b143fdf73df35d722010dce5c292c2219652e8f9 Mon Sep 17 00:00:00 2001 From: Catherine Fang Date: Tue, 2 Jan 2024 13:59:35 -0500 Subject: [PATCH 4/5] Add ci job for validating stable metrics Add ci job for validating stable metrics --- .github/workflows/ci.yml | 21 +++++++++++++++++++ Makefile | 6 ++++++ tests/fix-stability.sh | 17 +++++++++++++++ .../testdata/test-stable-metrics-list.yaml | 6 ++++++ tests/validate-stability.sh | 20 ++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100755 tests/fix-stability.sh create mode 100644 tests/stablemetrics/testdata/test-stable-metrics-list.yaml create mode 100755 tests/validate-stability.sh 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/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/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 From 57c58115002f0e95e5edd027a8c07eb06b9285a3 Mon Sep 17 00:00:00 2001 From: Catherine Fang Date: Tue, 2 Jan 2024 21:33:36 -0500 Subject: [PATCH 5/5] Add doc --- docs/design/validate-stable-metrics.md | 85 ++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 docs/design/validate-stable-metrics.md 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