Skip to content

Commit ffe4112

Browse files
committed
metrics: implement markers for metrics
1 parent 3caca5e commit ffe4112

13 files changed

+1189
-0
lines changed

pkg/metrics/markers/gvk.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package markers
17+
18+
import (
19+
"sigs.k8s.io/controller-tools/pkg/markers"
20+
21+
"sigs.k8s.io/controller-tools/pkg/metrics/internal/config"
22+
)
23+
24+
const (
25+
// GVKMarkerName is the marker for a GVK. Without a set GVKMarkerName the
26+
// generator will not generate any configuration for this GVK.
27+
GVKMarkerName = "Metrics:gvk"
28+
)
29+
30+
func init() {
31+
MarkerDefinitions = append(
32+
MarkerDefinitions,
33+
must(markers.MakeDefinition(GVKMarkerName, markers.DescribesType, gvkMarker{})).
34+
help(gvkMarker{}.Help()),
35+
)
36+
}
37+
38+
// +controllertools:marker:generateHelp:category=Metrics
39+
40+
// gvkMarker enables the creation of a custom resource configuration entry and uses the given prefix for the metrics if configured.
41+
type gvkMarker struct {
42+
// NamePrefix specifies the prefix for all metrics of this resource.
43+
// Note: This field directly maps to the metricNamePrefix field in the resource's custom resource configuration.
44+
NamePrefix string `marker:"namePrefix,optional"`
45+
}
46+
47+
var _ ResourceMarker = gvkMarker{}
48+
49+
func (n gvkMarker) ApplyToResource(resource *config.Resource) error {
50+
if n.NamePrefix != "" {
51+
resource.MetricNamePrefix = &n.NamePrefix
52+
}
53+
return nil
54+
}

pkg/metrics/markers/helper.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package markers
17+
18+
import (
19+
"fmt"
20+
21+
"k8s.io/client-go/util/jsonpath"
22+
ctrlmarkers "sigs.k8s.io/controller-tools/pkg/markers"
23+
24+
"sigs.k8s.io/controller-tools/pkg/metrics/internal/config"
25+
)
26+
27+
type markerDefinitionWithHelp struct {
28+
*ctrlmarkers.Definition
29+
Help *ctrlmarkers.DefinitionHelp
30+
}
31+
32+
func must(def *ctrlmarkers.Definition, err error) *markerDefinitionWithHelp {
33+
return &markerDefinitionWithHelp{
34+
Definition: ctrlmarkers.Must(def, err),
35+
}
36+
}
37+
38+
func (d *markerDefinitionWithHelp) help(help *ctrlmarkers.DefinitionHelp) *markerDefinitionWithHelp {
39+
d.Help = help
40+
return d
41+
}
42+
43+
func (d *markerDefinitionWithHelp) Register(reg *ctrlmarkers.Registry) error {
44+
if err := reg.Register(d.Definition); err != nil {
45+
return err
46+
}
47+
if d.Help != nil {
48+
reg.AddHelp(d.Definition, d.Help)
49+
}
50+
return nil
51+
}
52+
53+
// jsonPath is a simple JSON path, i.e. without array notation.
54+
type jsonPath string
55+
56+
// Parse is implemented to overwrite how json.Marshal and json.Unmarshal handles
57+
// this type and parses the string to a string array instead. It is inspired by
58+
// `kubectl explain` parsing the json path parameter.
59+
// xref: https://github.com/kubernetes/kubectl/blob/release-1.28/pkg/explain/explain.go#L35
60+
func (j jsonPath) Parse() ([]string, error) {
61+
ret := []string{}
62+
63+
jpp, err := jsonpath.Parse("JSONPath", `{`+string(j)+`}`)
64+
if err != nil {
65+
return nil, fmt.Errorf("parse JSONPath: %w", err)
66+
}
67+
68+
// Because of the way the jsonpath library works, the schema of the parser is [][]NodeList
69+
// meaning we need to get the outer node list, make sure it's only length 1, then get the inner node
70+
// list, and only then can we look at the individual nodes themselves.
71+
outerNodeList := jpp.Root.Nodes
72+
if len(outerNodeList) > 1 {
73+
return nil, fmt.Errorf("must pass in 1 jsonpath string, got %d", len(outerNodeList))
74+
}
75+
76+
list, ok := outerNodeList[0].(*jsonpath.ListNode)
77+
if !ok {
78+
return nil, fmt.Errorf("unable to typecast to jsonpath.ListNode")
79+
}
80+
for _, n := range list.Nodes {
81+
nf, ok := n.(*jsonpath.FieldNode)
82+
if !ok {
83+
return nil, fmt.Errorf("unable to typecast to jsonpath.NodeField")
84+
}
85+
ret = append(ret, nf.Value)
86+
}
87+
88+
return ret, nil
89+
}
90+
91+
func newMetricMeta(basePath []string, j jsonPath, jsonLabelsFromPath map[string]jsonPath) (config.MetricMeta, error) {
92+
path := basePath
93+
if j != "" {
94+
valueFrom, err := j.Parse()
95+
if err != nil {
96+
return config.MetricMeta{}, fmt.Errorf("failed to parse JSONPath %q", j)
97+
}
98+
if len(valueFrom) > 0 {
99+
path = append(path, valueFrom...)
100+
}
101+
}
102+
103+
labelsFromPath := map[string][]string{}
104+
for k, v := range jsonLabelsFromPath {
105+
path := []string{}
106+
var err error
107+
if v != "." {
108+
path, err = v.Parse()
109+
if err != nil {
110+
return config.MetricMeta{}, fmt.Errorf("failed to parse JSONPath %q", v)
111+
}
112+
}
113+
labelsFromPath[k] = path
114+
}
115+
116+
return config.MetricMeta{
117+
Path: path,
118+
LabelsFromPath: labelsFromPath,
119+
}, nil
120+
}

pkg/metrics/markers/helper_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package markers
17+
18+
import (
19+
"reflect"
20+
"testing"
21+
22+
"sigs.k8s.io/controller-tools/pkg/metrics/internal/config"
23+
)
24+
25+
func Test_jsonPath_Parse(t *testing.T) {
26+
tests := []struct {
27+
name string
28+
j jsonPath
29+
want []string
30+
wantErr bool
31+
}{
32+
{
33+
name: "empty input",
34+
j: "",
35+
want: []string{},
36+
wantErr: false,
37+
},
38+
{
39+
name: "dot input",
40+
j: ".",
41+
want: []string{""},
42+
wantErr: false,
43+
},
44+
{
45+
name: "some path input",
46+
j: ".foo.bar",
47+
want: []string{"foo", "bar"},
48+
wantErr: false,
49+
},
50+
{
51+
name: "invalid character ,",
52+
j: ".foo,.bar",
53+
wantErr: true,
54+
},
55+
{
56+
name: "invalid closure",
57+
j: "{.foo}",
58+
wantErr: true,
59+
},
60+
}
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
got, err := tt.j.Parse()
64+
if (err != nil) != tt.wantErr {
65+
t.Errorf("jsonPath.Parse() error = %v, wantErr %v", err, tt.wantErr)
66+
return
67+
}
68+
if !reflect.DeepEqual(got, tt.want) {
69+
t.Errorf("jsonPath.Parse() = %v, want %v", got, tt.want)
70+
}
71+
})
72+
}
73+
}
74+
75+
func Test_newMetricMeta(t *testing.T) {
76+
tests := []struct {
77+
name string
78+
basePath []string
79+
j jsonPath
80+
jsonLabelsFromPath map[string]jsonPath
81+
want config.MetricMeta
82+
}{
83+
{
84+
name: "with basePath and jsonpath, without jsonLabelsFromPath",
85+
basePath: []string{"foo"},
86+
j: jsonPath(".bar"),
87+
jsonLabelsFromPath: map[string]jsonPath{},
88+
want: config.MetricMeta{
89+
Path: []string{"foo", "bar"},
90+
LabelsFromPath: map[string][]string{},
91+
},
92+
},
93+
{
94+
name: "with basePath, jsonpath and jsonLabelsFromPath",
95+
basePath: []string{"foo"},
96+
j: jsonPath(".bar"),
97+
jsonLabelsFromPath: map[string]jsonPath{"some": ".label.from.path"},
98+
want: config.MetricMeta{
99+
Path: []string{"foo", "bar"},
100+
LabelsFromPath: map[string][]string{
101+
"some": {"label", "from", "path"},
102+
},
103+
},
104+
},
105+
{
106+
name: "no basePath, jsonpath and jsonLabelsFromPath",
107+
basePath: []string{},
108+
j: jsonPath(""),
109+
jsonLabelsFromPath: map[string]jsonPath{},
110+
want: config.MetricMeta{
111+
Path: []string{},
112+
LabelsFromPath: map[string][]string{},
113+
},
114+
},
115+
}
116+
for _, tt := range tests {
117+
t.Run(tt.name, func(t *testing.T) {
118+
if got, _ := newMetricMeta(tt.basePath, tt.j, tt.jsonLabelsFromPath); !reflect.DeepEqual(got, tt.want) {
119+
t.Errorf("newMetricMeta() = %v, want %v", got, tt.want)
120+
}
121+
})
122+
}
123+
}

pkg/metrics/markers/labelfrompath.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package markers
17+
18+
import (
19+
"errors"
20+
"fmt"
21+
22+
"sigs.k8s.io/controller-tools/pkg/markers"
23+
24+
"sigs.k8s.io/controller-tools/pkg/metrics/internal/config"
25+
)
26+
27+
const (
28+
labelFromPathMarkerName = "Metrics:labelFromPath"
29+
)
30+
31+
func init() {
32+
MarkerDefinitions = append(
33+
MarkerDefinitions,
34+
must(markers.MakeDefinition(labelFromPathMarkerName, markers.DescribesType, labelFromPathMarker{})).
35+
help(labelFromPathMarker{}.Help()),
36+
must(markers.MakeDefinition(labelFromPathMarkerName, markers.DescribesField, labelFromPathMarker{})).
37+
help(labelFromPathMarker{}.Help()),
38+
)
39+
}
40+
41+
// +controllertools:marker:generateHelp:category=Metrics
42+
43+
// labelFromPathMarker specifies additional labels for all metrics of this field or type.
44+
type labelFromPathMarker struct {
45+
// Name specifies the name of the label.
46+
Name string
47+
// JSONPath specifies the relative path to the value for the label.
48+
JSONPath jsonPath `marker:"JSONPath"`
49+
}
50+
51+
var _ ResourceMarker = labelFromPathMarker{}
52+
53+
func (n labelFromPathMarker) ApplyToResource(resource *config.Resource) error {
54+
if resource == nil {
55+
return errors.New("expected resource to not be nil")
56+
}
57+
58+
jsonPathElems, err := n.JSONPath.Parse()
59+
if err != nil {
60+
return err
61+
}
62+
63+
if resource.LabelsFromPath == nil {
64+
resource.LabelsFromPath = map[string][]string{}
65+
}
66+
67+
if jsonPath, labelExists := resource.LabelsFromPath[n.Name]; labelExists {
68+
if len(jsonPathElems) != len(jsonPath) {
69+
return fmt.Errorf("duplicate definition for label %q", n.Name)
70+
}
71+
for i, v := range jsonPath {
72+
if v != jsonPathElems[i] {
73+
return fmt.Errorf("duplicate definition for label %q", n.Name)
74+
}
75+
}
76+
}
77+
78+
resource.LabelsFromPath[n.Name] = jsonPathElems
79+
return nil
80+
}

0 commit comments

Comments
 (0)