Skip to content

Commit 46f36a3

Browse files
Merge pull request #2858 from Kavinjsir/feat/cutom-metrics-experiment
✨ Experimental practice to scaffold dashboard manifest for custom metrics
2 parents e4fecea + 001daab commit 46f36a3

File tree

12 files changed

+693
-2
lines changed

12 files changed

+693
-2
lines changed

docs/book/src/plugins/grafana-v1-alpha.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,48 @@ See an example of how to use the plugin in your project:
132132
- Per-second rate of retries handled by workqueue
133133
- Sample: <img width="914" src="https://user-images.githubusercontent.com/18136486/180360101-411c81e9-d54e-4b21-bbb0-e3f94fcf48cb.png">
134134

135+
### Visualize Custom Metrics
136+
137+
The Grafana plugin supports scaffolding manifests for custom metrics.
138+
139+
#### Generate Config Template
140+
141+
When the plugin is triggered for the first time, `grafana/custom-metrics/config.yaml` is generated.
142+
143+
```yaml
144+
---
145+
customMetrics:
146+
# - metric: # Raw custom metric (required)
147+
# type: # Metric type: counter/gauge/histogram (required)
148+
# expr: # Prom_ql for the metric (optional)
149+
```
150+
151+
#### Add Custom Metrics to Config
152+
153+
You can enter multiple custom metrics in the file. For each element, you need to specify the `metric` and its `type`.
154+
The Grafana plugin can automatically generate `expr` for visualization.
155+
Alternatively, you can provide `expr` and the plugin will use the specified one directly.
156+
157+
```yaml
158+
---
159+
customMetrics:
160+
- metric: memcached_operator_reconcile_total # Raw custom metric (required)
161+
type: counter # Metric type: counter/gauge/histogram (required)
162+
- metric: memcached_operator_reconcile_time_seconds_bucket
163+
type: histogram
164+
```
165+
166+
#### Scaffold Manifest
167+
168+
Once `config.yaml` is configured, you can run `kubebuilder edit --plugins grafana.kubebuilder.io/v1-alpha` again.
169+
This time, the plugin will generate `grafana/custom-metrics/custom-metrics-dashboard.json`, which can be imported to Grafana UI.
170+
171+
#### Show case:
172+
173+
See an example of how to visualize your custom metrics:
174+
175+
![output2](https://user-images.githubusercontent.com/18136486/186933170-d2e0de71-e079-4d1b-906a-99a549d66ebf.gif)
176+
135177
## Subcommands
136178

137179
The Grafana plugin implements the following subcommands:

pkg/plugin/util/util.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,14 @@ func ReplaceRegexInFile(path, match, replace string) error {
242242
}
243243
return nil
244244
}
245+
246+
// HasFileContentWith check if given `text` can be found in file
247+
func HasFileContentWith(path, text string) (bool, error) {
248+
// nolint:gosec
249+
contents, err := ioutil.ReadFile(path)
250+
if err != nil {
251+
return false, err
252+
}
253+
254+
return strings.Contains(string(contents), text), nil
255+
}

pkg/plugins/optional/grafana/v1alpha/scaffolds/edit.go

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,22 @@ package scaffolds
1818

1919
import (
2020
"fmt"
21+
"io"
22+
"io/ioutil"
23+
"os"
24+
"strings"
2125

2226
"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
2327
"sigs.k8s.io/kubebuilder/v3/pkg/plugins"
2428
"sigs.k8s.io/kubebuilder/v3/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates"
29+
30+
"sigs.k8s.io/yaml"
2531
)
2632

2733
var _ plugins.Scaffolder = &editScaffolder{}
2834

35+
const configFilePath = "grafana/custom-metrics/config.yaml"
36+
2937
type editScaffolder struct {
3038
// fs is the filesystem that will be used by the scaffolder
3139
fs machinery.Filesystem
@@ -41,15 +49,121 @@ func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
4149
s.fs = fs
4250
}
4351

52+
func fileExist(configFilePath string) bool {
53+
if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
54+
return false
55+
}
56+
return true
57+
}
58+
59+
func loadConfig(configPath string) ([]templates.CustomMetricItem, error) {
60+
if !fileExist(configPath) {
61+
return nil, nil
62+
}
63+
64+
// nolint:gosec
65+
f, err := os.Open(configPath)
66+
if err != nil {
67+
return nil, fmt.Errorf("error loading plugin config: %w", err)
68+
}
69+
70+
items, err := configReader(f)
71+
72+
if err := f.Close(); err != nil {
73+
return nil, fmt.Errorf("could not close config.yaml: %w", err)
74+
}
75+
76+
return items, err
77+
}
78+
79+
func configReader(reader io.Reader) ([]templates.CustomMetricItem, error) {
80+
yamlFile, err := ioutil.ReadAll(reader)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
config := templates.CustomMetricsConfig{}
86+
87+
err = yaml.Unmarshal(yamlFile, &config)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
validatedMetricItems := validateCustomMetricItems(config.CustomMetrics)
93+
94+
return validatedMetricItems, nil
95+
}
96+
97+
func validateCustomMetricItems(rawItems []templates.CustomMetricItem) []templates.CustomMetricItem {
98+
// 1. Filter items of missing `Metric` or `Type`
99+
filterResult := []templates.CustomMetricItem{}
100+
for _, item := range rawItems {
101+
if hasFields(item) {
102+
filterResult = append(filterResult, item)
103+
}
104+
}
105+
106+
// 2. Fill Expr if missing
107+
validatedItems := make([]templates.CustomMetricItem, len(filterResult))
108+
for i, item := range filterResult {
109+
validatedItems[i] = fillMissingExpr(item)
110+
}
111+
112+
return validatedItems
113+
}
114+
115+
func hasFields(item templates.CustomMetricItem) bool {
116+
// If `Expr` exists, return true
117+
if item.Expr != "" {
118+
return true
119+
}
120+
121+
// If `Metric` & valid `Type` exists, return true
122+
metricType := strings.ToLower(item.Type)
123+
if item.Metric != "" && (metricType == "counter" || metricType == "gauge" || metricType == "histogram") {
124+
return true
125+
}
126+
127+
return false
128+
}
129+
130+
// TODO: Prom_ql exprs can improved to be more pratical and applicable
131+
func fillMissingExpr(item templates.CustomMetricItem) templates.CustomMetricItem {
132+
if item.Expr == "" {
133+
switch strings.ToLower(item.Type) {
134+
case "counter":
135+
item.Expr = "sum(rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)`
136+
case "histogram":
137+
// nolint: lll
138+
item.Expr = "histogram_quantile(0.90, sum by(instance, le) (rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])))`
139+
default: // gauge
140+
item.Expr = item.Metric
141+
}
142+
}
143+
return item
144+
}
145+
44146
// Scaffold implements cmdutil.Scaffolder
45147
func (s *editScaffolder) Scaffold() error {
46148
fmt.Println("Generating Grafana manifests to visualize controller status...")
47149

48150
// Initialize the machinery.Scaffold that will write the files to disk
49151
scaffold := machinery.NewScaffold(s.fs)
50152

51-
return scaffold.Execute(
153+
configPath := string(configFilePath)
154+
155+
var templatesBuilder = []machinery.Builder{
52156
&templates.RuntimeManifest{},
53157
&templates.ResourcesManifest{},
54-
)
158+
&templates.CustomMetricsConfigManifest{ConfigPath: configPath},
159+
}
160+
161+
configItems, err := loadConfig(configPath)
162+
if err == nil && len(configItems) > 0 {
163+
templatesBuilder = append(templatesBuilder, &templates.CustomMetricsDashManifest{Items: configItems})
164+
} else if err != nil {
165+
fmt.Fprintf(os.Stderr, "Error on scaffolding manifest for custom metris:\n%v", err)
166+
}
167+
168+
return scaffold.Execute(templatesBuilder...)
55169
}

pkg/plugins/optional/grafana/v1alpha/scaffolds/init.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@ func (s *initScaffolder) Scaffold() error {
5151
return scaffold.Execute(
5252
&templates.RuntimeManifest{},
5353
&templates.ResourcesManifest{},
54+
&templates.CustomMetricsConfigManifest{ConfigPath: string(configFilePath)},
5455
)
5556
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
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+
17+
package templates
18+
19+
import (
20+
"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
21+
)
22+
23+
var _ machinery.Template = &CustomMetricsConfigManifest{}
24+
25+
// Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder
26+
type CustomMetricsConfigManifest struct {
27+
machinery.TemplateMixin
28+
ConfigPath string
29+
}
30+
31+
// SetTemplateDefaults implements file.Template
32+
func (f *CustomMetricsConfigManifest) SetTemplateDefaults() error {
33+
f.Path = f.ConfigPath
34+
35+
f.TemplateBody = customMetricsConfigTemplate
36+
37+
f.IfExistsAction = machinery.SkipFile
38+
39+
return nil
40+
}
41+
42+
// nolint: lll
43+
const customMetricsConfigTemplate = `---
44+
customMetrics:
45+
# - metric: # Raw custom metric (required)
46+
# type: # Metric type: counter/gauge/histogram (required)
47+
# expr: # Prom_ql for the metric (optional)
48+
#
49+
#
50+
# Example:
51+
# ---
52+
# customMetrics:
53+
# - metric: foo_bar
54+
# type: histogram
55+
# expr: histogram_quantile(0.90, sum by(instance, le) (rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m])))
56+
`

0 commit comments

Comments
 (0)