Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
485 changes: 485 additions & 0 deletions docs/metrics/extend/customresourcestate-metrics.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0
github.com/go-logr/logr v1.4.3
github.com/gobuffalo/flect v1.0.3
github.com/google/cel-go v0.26.0
github.com/google/go-cmp v0.7.0
github.com/netresearch/go-cron v0.14.0
github.com/oklog/run v1.2.0
Expand Down Expand Up @@ -114,7 +115,6 @@ require (
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/cel-go v0.26.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-jsonnet v0.21.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
Expand Down
46 changes: 46 additions & 0 deletions pkg/cel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CEL Extensions

CEL extension library for kube-state-metrics.

## Package Structure

* `pkg/cel/` - Type definitions
* `pkg/cel/library/` - CEL library implementation

## Types

### WithLabels Type

`WithLabels(value, labels)` wraps a metric value with additional labels. Non-string label values are automatically stringified.

```cel
WithLabels(100.0, {})
WithLabels(42, {'severity': 'high'})
WithLabels(double(value) * 10.0, {'multiplied': 'true'})
WithLabels(v.ready, {'replicas': v.replicas}) // numeric label value is stringified
```

Fields:

* `Val` - metric value (converted to float64 by extractor)
* `AdditionalLabels` - labels to add to metric

## Usage

```go
import (
"github.com/google/cel-go/cel"
"k8s.io/kube-state-metrics/v2/pkg/cel/library"
)

env, err := cel.NewEnv(
library.KSM(),
cel.Variable("value", cel.DynType),
)
```

## Extending

Add new functions to `ksmLibraryDecls` in [library/library.go](library/library.go).

New types go in this package with ref.Val implementation, then register in library's `Types()` method.
105 changes: 105 additions & 0 deletions pkg/cel/library/library.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
Copyright 2026 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package library

import (
"fmt"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"

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

// KSM provides CEL custom functions for kube-state-metrics.
//
// # WithLabels
//
// Wraps a metric value with additional labels. Non-string label values are
// automatically stringified (via fmt "%v").
//
// WithLabels(<any>, <map<string, any>>) <WithLabels>
//
// Examples:
//
// WithLabels(100.0, {}) // returns value 100.0 with no additional labels
// WithLabels(42, {'severity': 'high'}) // returns value 42 with label severity=high
// WithLabels(double(value) * 10.0, {'multiplied': 'true'}) // returns computed value with label
// WithLabels(v.ready, {'replicas': v.replicas}) // numeric label stringified
func KSM() cel.EnvOption {
return cel.Lib(ksmLib)
}

var ksmLib = &ksm{}

type ksm struct{}

func (*ksm) LibraryName() string {
return "kubestatemetrics"
}

func (*ksm) Types() []*cel.Type {
return []*cel.Type{ksmcel.WithLabelsObjectType}
}

var ksmLibraryDecls = map[string][]cel.FunctionOpt{
"WithLabels": {
cel.Overload("withlabels_any_map",
[]*cel.Type{cel.DynType, cel.MapType(cel.StringType, cel.DynType)},
ksmcel.WithLabelsObjectType,
cel.BinaryBinding(withLabelsConstructor)),
},
}

func (*ksm) CompileOptions() []cel.EnvOption {
options := []cel.EnvOption{cel.Types(ksmcel.WithLabelsObjectType)}
for name, overloads := range ksmLibraryDecls {
options = append(options, cel.Function(name, overloads...))
}
options = append(options, cel.Container("kubestatemetrics"))
return options
}

func (*ksm) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}

// withLabelsConstructor is the implementation of the WithLabels constructor function.
// It takes a value and a map of labels and returns a WithLabels result.
func withLabelsConstructor(val, labels ref.Val) ref.Val {
result := &ksmcel.WithLabels{
Val: val.Value(),
AdditionalLabels: make(map[string]string),
}

// Extract labels from the map
if labelsMap, ok := labels.(traits.Mapper); ok {
it := labelsMap.Iterator()
for it.HasNext() == types.True {
key := it.Next()
value := labelsMap.Get(key)

keyStr := fmt.Sprintf("%v", key.Value())
valueStr := fmt.Sprintf("%v", value.Value())
result.AdditionalLabels[keyStr] = valueStr
}
}

return result
}
93 changes: 93 additions & 0 deletions pkg/cel/withlabels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2026 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cel

import (
"fmt"
"reflect"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)

var _ ref.Val = &WithLabels{}

// WithLabels represents a metric value with additional labels.
// Implements ref.Val.
type WithLabels struct {
Val interface{}
AdditionalLabels map[string]string
}

var (
// WithLabelsObjectType is the CEL type representation for WithLabels objects.
WithLabelsObjectType = cel.ObjectType("kubestatemetrics.WithLabels")
)

// ConvertToNative implements the ref.Val interface method.
func (r *WithLabels) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
if reflect.TypeOf(r).AssignableTo(typeDesc) {
return r, nil
}
return nil, fmt.Errorf("type conversion error from 'WithLabels' to '%v'", typeDesc)
}

// ConvertToType implements the ref.Val interface method.
func (r *WithLabels) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case WithLabelsObjectType:
return r
case types.TypeType:
return WithLabelsObjectType
}
return types.NewErr("type conversion error from '%s' to '%s'", WithLabelsObjectType, typeVal)
}

// Equal implements the ref.Val interface method.
func (r *WithLabels) Equal(other ref.Val) ref.Val {
otherResult, ok := other.(*WithLabels)
if !ok {
return types.False
}

if !reflect.DeepEqual(r.Val, otherResult.Val) {
return types.False
}

if len(r.AdditionalLabels) != len(otherResult.AdditionalLabels) {
return types.False
}

for k, v := range r.AdditionalLabels {
if otherResult.AdditionalLabels[k] != v {
return types.False
}
}

return types.True
}

// Type implements the ref.Val interface method.
func (r *WithLabels) Type() ref.Type {
return WithLabelsObjectType
}

// Value implements the ref.Val interface method.
func (r *WithLabels) Value() interface{} {
return r
}
Loading