diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go index ec4c44abbbc..5b9a1b5224c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go @@ -31,6 +31,7 @@ import ( apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/library" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics" celmodel "k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model" ) @@ -99,6 +100,9 @@ func getBaseEnv() (*cel.Env, error) { // - nil Program, nil Error: The provided rule was empty so compilation was not attempted // perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit as input. func Compile(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) ([]CompilationResult, error) { + t := time.Now() + defer metrics.Metrics.ObserveCompilation(time.Since(t)) + if len(s.Extensions.XValidations) == 0 { return nil, nil } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics/metrics.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics/metrics.go new file mode 100644 index 00000000000..3ddb76cdfed --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics/metrics.go @@ -0,0 +1,72 @@ +/* +Copyright 2022 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 metrics + +import ( + "time" + + "k8s.io/component-base/metrics" + "k8s.io/component-base/metrics/legacyregistry" +) + +// TODO(jiahuif) CEL is to be used in multiple components, revise naming when that happens. +const ( + namespace = "apiserver" + subsystem = "cel" +) + +// Metrics provides access to CEL metrics. +var Metrics = newCelMetrics() + +type CelMetrics struct { + compilationTime *metrics.Histogram + evaluationTime *metrics.Histogram +} + +func newCelMetrics() *CelMetrics { + m := &CelMetrics{ + compilationTime: metrics.NewHistogram(&metrics.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "compilation_duration_seconds", + StabilityLevel: metrics.ALPHA, + }), + evaluationTime: metrics.NewHistogram(&metrics.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "evaluation_duration_seconds", + StabilityLevel: metrics.ALPHA, + }), + } + + legacyregistry.MustRegister(m.compilationTime) + legacyregistry.MustRegister(m.evaluationTime) + + return m +} + +// ObserveCompilation records a CEL compilation with the time the compilation took. +func (m *CelMetrics) ObserveCompilation(elapsed time.Duration) { + seconds := elapsed.Seconds() + m.compilationTime.Observe(seconds) +} + +// ObserveEvaluation records a CEL evaluation with the time the evaluation took. +func (m *CelMetrics) ObserveEvaluation(elapsed time.Duration) { + seconds := elapsed.Seconds() + m.evaluationTime.Observe(seconds) +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics/metrics_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics/metrics_test.go new file mode 100644 index 00000000000..11fb2356309 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics/metrics_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2022 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 metrics + +import ( + "math" + "testing" + "time" + + "k8s.io/component-base/metrics/legacyregistry" +) + +func TestObserveCompilation(t *testing.T) { + defer legacyregistry.Reset() + Metrics.ObserveCompilation(2 * time.Second) + c, s := gatherHistogram(t, "apiserver_cel_compilation_duration_seconds") + if c != 1 { + t.Errorf("unexpected count: %v", c) + } + if math.Abs(s-2.0) > 1e-7 { + t.Fatalf("incorrect sum: %v", s) + } +} + +func TestObserveEvaluation(t *testing.T) { + defer legacyregistry.Reset() + Metrics.ObserveEvaluation(2 * time.Second) + c, s := gatherHistogram(t, "apiserver_cel_evaluation_duration_seconds") + if c != 1 { + t.Errorf("unexpected count: %v", c) + } + if math.Abs(s-2.0) > 1e-7 { + t.Fatalf("incorrect sum: %v", s) + } +} + +func gatherHistogram(t *testing.T, name string) (count uint64, sum float64) { + metrics, err := legacyregistry.DefaultGatherer.Gather() + if err != nil { + t.Fatalf("Failed to gather metrics: %s", err) + } + for _, mf := range metrics { + if mf.GetName() == name { + for _, m := range mf.GetMetric() { + h := m.GetHistogram() + count += h.GetSampleCount() + sum += h.GetSampleSum() + } + return + } + } + t.Fatalf("metric not found: %v", name) + return 0, 0 +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go index 8b0eceabf0e..9e57daafdb3 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go @@ -22,6 +22,7 @@ import ( "math" "reflect" "strings" + "time" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" @@ -29,6 +30,7 @@ import ( apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics" "k8s.io/apiextensions-apiserver/third_party/forked/celopenapi/model" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -113,6 +115,8 @@ func validator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) * // Most callers can ignore the returned remainingBudget value unless another validate call is going to be made // context is passed for supporting context cancellation during cel validation func (s *Validator) Validate(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) { + t := time.Now() + defer metrics.Metrics.ObserveEvaluation(time.Since(t)) remainingBudget = costBudget if s == nil || obj == nil { return nil, remainingBudget diff --git a/vendor/modules.txt b/vendor/modules.txt index a4147a1be9b..6ead768df4a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1428,6 +1428,7 @@ k8s.io/apiextensions-apiserver/pkg/apiserver/conversion k8s.io/apiextensions-apiserver/pkg/apiserver/schema k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/library +k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype k8s.io/apiextensions-apiserver/pkg/apiserver/schema/objectmeta